- update dorm

master
李光春 2 years ago
parent ea7163af0e
commit 6c8a603646

@ -21,6 +21,8 @@ require (
github.com/go-playground/universal-translator v0.18.0
github.com/go-playground/validator/v10 v10.11.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-rel/mysql v0.8.0
github.com/go-rel/rel v0.38.0
github.com/go-sql-driver/mysql v1.6.0
github.com/godror/godror v0.33.3
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.12+incompatible
@ -81,6 +83,7 @@ require (
github.com/glebarez/go-sqlite v1.17.3 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-rel/sql v0.11.0 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/goccy/go-json v0.9.8 // indirect
github.com/godror/knownpb v0.1.0 // indirect
@ -117,8 +120,9 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/saracen/go7z-fixtures v0.0.0-20190623165746-aa6b8fba1d2f // indirect
github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f // indirect
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/stretchr/testify v1.7.2 // indirect
github.com/stretchr/testify v1.8.0 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect

@ -136,6 +136,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
@ -171,6 +172,14 @@ github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4
github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-rel/mysql v0.8.0 h1:4SnMA998SrGrPAWcrCiv6FwuduMpOpGwQXoVgy2xFQE=
github.com/go-rel/mysql v0.8.0/go.mod h1:E6E064+zA6JISGrnJfvhc4QhgEirbTt0iAO7sBttNAc=
github.com/go-rel/primaryreplica v0.4.0 h1:lhU+4dh0/sDQEs602Chiz0SJDXewPU06baWQlx7oB3c=
github.com/go-rel/rel v0.37.0/go.mod h1:Zq18pQqXZbDh2JBCo29jgt+y90nZWkUvI+W9Ls29ans=
github.com/go-rel/rel v0.38.0 h1:XooFDMrzHNaZSNvH1ZrEpcn/7TvPz37z1kA66N3Ahjo=
github.com/go-rel/rel v0.38.0/go.mod h1:Zq18pQqXZbDh2JBCo29jgt+y90nZWkUvI+W9Ls29ans=
github.com/go-rel/sql v0.11.0 h1:MeyoMtfDpn9sZSQNMuo7YbN8M/z74eIy9UMN3m6uonQ=
github.com/go-rel/sql v0.11.0/go.mod h1:L4XKALdxaEGwT7ngflceoHVFSooUJap5TO/Yu8FGKJI=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@ -510,6 +519,7 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nilorg/sdk v0.0.0-20220617065147-3001fb840741 h1:oqg84OxQrU/bdn22BOceI5ehavqCY3GsRUyp74UM8Cw=
github.com/nilorg/sdk v0.0.0-20220617065147-3001fb840741/go.mod h1:X1swpPdqguAZaBDoEPyEWHSsJii0YQ1o+3piMv6W3JU=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
@ -519,9 +529,14 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
@ -614,6 +629,8 @@ github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f/go.mod h1:LyBTu
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE=
github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@ -641,16 +658,20 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
@ -783,9 +804,11 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -829,7 +852,10 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -840,6 +866,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -897,6 +924,7 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=

@ -0,0 +1,16 @@
package dorm
import (
"github.com/go-rel/rel"
)
type ConfigRelClient struct {
Dns string // 地址
}
// RelClient
// https://go-rel.github.io/
type RelClient struct {
Db *rel.Repository // 驱动
config *ConfigRelClient // 配置
}

@ -0,0 +1,27 @@
package dorm
import (
"errors"
"fmt"
"github.com/go-rel/mysql"
"github.com/go-rel/rel"
_ "github.com/go-sql-driver/mysql"
)
func NewRelMysqlClient(config *ConfigRelClient) (*RelClient, error) {
var err error
c := &RelClient{config: config}
adapter, err := mysql.Open(c.config.Dns)
defer adapter.Close()
if err != nil {
return nil, errors.New(fmt.Sprintf("连接失败:%v", err))
}
repo := rel.New(adapter)
c.Db = &repo
return c, nil
}

@ -0,0 +1,12 @@
version = 1
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_root = "github.com/go-rel/mysql"
[[transformers]]
name = "gofmt"
enabled = true

@ -0,0 +1,8 @@
vendor
.tool-versions
*.db
.vscode/
debug.test
.idea/
*.out
*.test

@ -0,0 +1,8 @@
builds:
- skip: true
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 REL
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,89 @@
# mysql
[![GoDoc](https://godoc.org/github.com/go-rel/mysql?status.svg)](https://pkg.go.dev/github.com/go-rel/mysql)
[![Tesst](https://github.com/go-rel/mysql/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/go-rel/mysql/actions/workflows/test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-rel/mysql)](https://goreportcard.com/report/github.com/go-rel/mysql)
[![codecov](https://codecov.io/gh/go-rel/mysql/branch/main/graph/badge.svg?token=56qOCsVPJF)](https://codecov.io/gh/go-rel/mysql)
[![Gitter chat](https://badges.gitter.im/go-rel/rel.png)](https://gitter.im/go-rel/rel)
MySQL adapter for REL.
## Example
```go
package main
import (
"context"
_ "github.com/go-sql-driver/mysql"
"github.com/go-rel/mysql"
"github.com/go-rel/rel"
)
func main() {
// open mysql connection.
// note: `clientFoundRows=true` is required for update and delete to works correctly.
adapter, err := mysql.Open("root@(127.0.0.1:3306)/rel_test?clientFoundRows=true&charset=utf8&parseTime=True&loc=Local")
if err != nil {
panic(err)
}
defer adapter.Close()
// initialize REL's repo.
repo := rel.New(adapter)
repo.Ping(context.TODO())
}
```
## Example Replication (Source/Replica)
```go
package main
import (
"context"
_ "github.com/go-sql-driver/mysql"
"github.com/go-rel/primaryreplica"
"github.com/go-rel/mysql"
"github.com/go-rel/rel"
)
func main() {
// open mysql connection.
// note: `clientFoundRows=true` is required for update and delete to works correctly.
adapter := primaryreplica.New(
mysql.MustOpen("root@(source:23306)/rel_test?charset=utf8&parseTime=True&loc=Local"),
mysql.MustOpen("root@(replica:23307)/rel_test?charset=utf8&parseTime=True&loc=Local"),
)
defer adapter.Close()
// initialize REL's repo.
repo := rel.New(adapter)
repo.Ping(context.TODO())
}
```
## Supported Driver
- github.com/go-sql-driver/mysql
## Supported Database
- MySQL 5 and 8
- MariaDB 10
## Testing
### Start MariaDB server in Docker
```console
docker run -it --rm -p 3307:3306 -e "MARIADB_ROOT_PASSWORD=test" -e "MARIADB_DATABASE=rel_test" mariadb:10
```
### Run tests
```console
MYSQL_DATABASE="root:test@tcp(localhost:3307)/rel_test" go test ./...
```

@ -0,0 +1,42 @@
version: '2.1'
services:
mariadb-master:
image: docker.io/bitnami/mariadb:10.6
ports:
- '23306:3306'
environment:
- MARIADB_REPLICATION_MODE=master
- MARIADB_REPLICATION_USER=repl_user
- MARIADB_USER=rel
- MARIADB_PASSWORD=rel
- MARIADB_DATABASE=rel_test
- MARIADB_ROOT_PASSWORD=rel
- ALLOW_EMPTY_PASSWORD=yes
healthcheck:
test: ['CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh']
interval: 15s
timeout: 5s
retries: 6
mariadb-slave:
image: docker.io/bitnami/mariadb:10.6
ports:
- '23307:3306'
depends_on:
- mariadb-master
environment:
- MARIADB_REPLICATION_MODE=slave
- MARIADB_REPLICATION_USER=repl_user
- MARIADB_USER=rel
- MARIADB_PASSWORD=rel
- MARIADB_DATABASE=rel_test
- MARIADB_MASTER_HOST=mariadb-master
- MARIADB_MASTER_PORT_NUMBER=3306
- MARIADB_MASTER_ROOT_PASSWORD=rel
- ALLOW_EMPTY_PASSWORD=yes
healthcheck:
test: ['CMD', '/opt/bitnami/scripts/mariadb/healthcheck.sh']
interval: 15s
timeout: 5s
retries: 6

@ -0,0 +1,144 @@
// Package mysql wraps mysql driver as an adapter for REL.
//
// Usage:
// // open mysql connection.
// // note: `clientFoundRows=true` is required for update and delete to works correctly.
// adapter, err := mysql.Open("root@(127.0.0.1:3306)/rel_test?clientFoundRows=true&charset=utf8&parseTime=True&loc=Local")
// if err != nil {
// panic(err)
// }
// defer adapter.Close()
//
// // initialize REL's repo.
// repo := rel.New(adapter)
package mysql
import (
"context"
db "database/sql"
"strings"
"github.com/go-rel/rel"
"github.com/go-rel/sql"
"github.com/go-rel/sql/builder"
)
// New mysql adapter using existing connection.
// Existing connection needs to be created with `clientFoundRows=true` options for update and delete to works correctly.
func New(database *db.DB) rel.Adapter {
var (
bufferFactory = builder.BufferFactory{ArgumentPlaceholder: "?", BoolTrueValue: "true", BoolFalseValue: "false", Quoter: Quote{}, ValueConverter: ValueConvert{}}
filterBuilder = builder.Filter{}
queryBuilder = builder.Query{BufferFactory: bufferFactory, Filter: filterBuilder}
onConflictBuilder = builder.OnConflict{Statement: "ON DUPLICATE KEY", UpdateStatement: "UPDATE", UseValues: true}
InsertBuilder = builder.Insert{BufferFactory: bufferFactory, InsertDefaultValues: true, OnConflict: onConflictBuilder}
insertAllBuilder = builder.InsertAll{BufferFactory: bufferFactory, OnConflict: onConflictBuilder}
updateBuilder = builder.Update{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder}
deleteBuilder = builder.Delete{BufferFactory: bufferFactory, Query: queryBuilder, Filter: filterBuilder}
ddlBufferFactory = builder.BufferFactory{InlineValues: true, BoolTrueValue: "true", BoolFalseValue: "false", Quoter: Quote{}, ValueConverter: ValueConvert{}}
ddlQueryBuilder = builder.Query{BufferFactory: ddlBufferFactory, Filter: filterBuilder}
tableBuilder = builder.Table{BufferFactory: ddlBufferFactory, ColumnMapper: columnMapper}
indexBuilder = builder.Index{BufferFactory: ddlBufferFactory, Query: ddlQueryBuilder, Filter: filterBuilder, DropIndexOnTable: true}
)
return &sql.SQL{
QueryBuilder: queryBuilder,
InsertBuilder: InsertBuilder,
InsertAllBuilder: insertAllBuilder,
UpdateBuilder: updateBuilder,
DeleteBuilder: deleteBuilder,
TableBuilder: tableBuilder,
IndexBuilder: indexBuilder,
IncrementFunc: incrementFunc,
ErrorMapper: errorMapper,
DB: database,
}
}
// Open mysql connection using dsn.
func Open(dsn string) (rel.Adapter, error) {
// force clientFoundRows=true
// this allows not found record check when updating a record.
if strings.ContainsRune(dsn, '?') {
dsn += "&clientFoundRows=true"
} else {
dsn += "?clientFoundRows=true"
}
var database, err = db.Open("mysql", dsn)
return New(database), err
}
// MustOpen mysql connection using dsn.
func MustOpen(dsn string) rel.Adapter {
var (
adapter, err = Open(dsn)
)
check(err)
return adapter
}
func incrementFunc(adapter sql.SQL) int {
var (
variable string
increment int
rows, err = adapter.DoQuery(context.TODO(), "SHOW VARIABLES LIKE 'auto_increment_increment';", nil)
)
check(err)
defer rows.Close()
rows.Next()
check(rows.Scan(&variable, &increment))
return increment
}
func errorMapper(err error) error {
if err == nil {
return nil
}
var (
msg = err.Error()
errCodeSep = ':'
errCodeIndex = strings.IndexRune(msg, errCodeSep)
)
if errCodeIndex < 0 {
errCodeIndex = 0
}
switch msg[:errCodeIndex] {
case "Error 1062":
return rel.ConstraintError{
Key: sql.ExtractString(msg, "key '", "'"),
Type: rel.UniqueConstraint,
Err: err,
}
case "Error 1452":
return rel.ConstraintError{
Key: sql.ExtractString(msg, "CONSTRAINT `", "`"),
Type: rel.ForeignKeyConstraint,
Err: err,
}
default:
return err
}
}
func columnMapper(column *rel.Column) (string, int, int) {
switch column.Type {
case rel.JSON:
return "JSON", 0, 0
default:
return sql.ColumnMapper(column)
}
}
func check(err error) {
if err != nil {
panic(err)
}
}

@ -0,0 +1,84 @@
package mysql
import (
"database/sql/driver"
"strings"
"time"
)
// Quote MySQL identifiers and literals.
type Quote struct{}
func (q Quote) ID(name string) string {
end := strings.IndexRune(name, 0)
if end > -1 {
name = name[:end]
}
return "`" + strings.ReplaceAll(name, "`", "``") + "`"
}
func (q Quote) Value(v interface{}) string {
switch v := v.(type) {
default:
panic("unsupported value")
case string:
// TODO: Need to check on connection for NO_BACKSLASH_ESCAPES
rv := []rune(v)
buf := make([]rune, len(rv)*2)
pos := 0
for i := 0; i < len(rv); i++ {
c := rv[i]
switch c {
case '\x00':
buf[pos] = '\\'
buf[pos+1] = '0'
pos += 2
case '\n':
buf[pos] = '\\'
buf[pos+1] = 'n'
pos += 2
case '\r':
buf[pos] = '\\'
buf[pos+1] = 'r'
pos += 2
case '\x1a':
buf[pos] = '\\'
buf[pos+1] = 'Z'
pos += 2
case '\'':
buf[pos] = '\\'
buf[pos+1] = '\''
pos += 2
case '"':
buf[pos] = '\\'
buf[pos+1] = '"'
pos += 2
case '\\':
buf[pos] = '\\'
buf[pos+1] = '\\'
pos += 2
default:
buf[pos] = c
pos++
}
}
return "'" + string(buf[:pos]) + "'"
}
}
// ValueConvert converts values to MySQL literals.
type ValueConvert struct{}
func (c ValueConvert) ConvertValue(v interface{}) (driver.Value, error) {
v, err := driver.DefaultParameterConverter.ConvertValue(v)
if err != nil {
return nil, err
}
switch v := v.(type) {
default:
return v, nil
case time.Time:
return v.Truncate(time.Microsecond).Format("2006-01-02 15:04:05.999999"), nil
}
}

@ -0,0 +1,4 @@
exclude_patterns:
- "**/vendor/"
- "**/*_test.go"
- adapter/specs/

@ -0,0 +1,12 @@
version = 1
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_root = "github.com/go-rel/rel"
[[transformers]]
name = "gofmt"
enabled = true

@ -0,0 +1,9 @@
vendor
.tool-versions
*.db
.vscode/
debug.test
.idea/
*.out
*.test
dist/

@ -0,0 +1,65 @@
before:
hooks:
- go mod download
- go generate ./...
builds:
- main: ./cmd/rel/main.go
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- 386
- amd64
- arm
- arm64
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^chore:'
nfpms:
- file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
homepage: https://go-rel.github.io
description: Modern Database Access Layer for Golang
maintainer: Muhammad Surya Asriadie <surya.asriadie@gmail.com>
license: MIT
vendor: REL
formats:
- apk
- deb
- rpm
dependencies:
- golang
brews:
- tap:
owner: go-rel
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
commit_author:
name: REL
homepage: "https://go-rel.github.io/"
description: "Database migration using REL"
license: "MIT"
folder: Formula
dependencies:
- name: golang
type: optional

@ -0,0 +1,12 @@
# How to contribute
I'm really glad you're reading this, because we need volunteer developers to help this project come to fruition.
Here's some way you on how you can contribute to this project:
- Report any bug, feature request and questions using issues.
- Contribute directly to the development, don't hestitate to take any task available on [projects](https://github.com/go-rel/rel/projects) page. You can use issues if you need further discussion and help about the implementation.
- Improvement to the documentation is always welcomed.
- Star and let the world know about this project.
Thanks :heart: :heart: :heart:

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Muhammad Surya Asriadie
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,44 @@
# REL
[![GoDoc](https://godoc.org/github.com/go-rel/rel?status.svg)](https://godoc.org/github.com/go-rel/rel)
[![Build Status](https://github.com/go-rel/rel/workflows/Build/badge.svg)](https://github.com/go-rel/rel/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-rel/rel)](https://goreportcard.com/report/github.com/go-rel/rel)
[![Maintainability](https://api.codeclimate.com/v1/badges/194611cc82f02edcda6e/maintainability)](https://codeclimate.com/github/go-rel/rel/maintainability)
[![Codecov](https://codecov.io/gh/go-rel/rel/branch/master/graph/badge.svg?token=0P505E1IWB)](https://codecov.io/gh/go-rel/rel)
[![Gitter chat](https://badges.gitter.im/go-rel/rel.png)](https://gitter.im/go-rel/rel)
> Modern Database Access Layer for Golang.
REL is golang orm-ish database layer for layered architecture. It's testable and comes with its own test library. REL also features extendable query builder that allows you to write query using builder or plain sql.
## Features
- Testable repository with builtin reltest package.
- Seamless nested transactions.
- Elegant, yet extendable query builder with mix of syntactic sugar.
- Supports Eager loading.
- Composite Primary Key.
- Multi adapter.
- Soft Deletion.
- Pagination.
- Schema Migration.
## Install
```bash
go get github.com/go-rel/rel
```
## Getting Started
- Guides [https://go-rel.github.io](https://go-rel.github.io)
## Examples
- [gin-example](https://github.com/go-rel/gin-example) - Todo Backend using Gin and REL
- [go-todo-backend](https://github.com/Fs02/go-todo-backend) - Todo Backend using Chi and REL
- [iris-example](https://github.com/iris-contrib/go-rel-iris-example) - Todo Backend using Iris and REL
## License
Released under the [MIT License](https://github.com/go-rel/rel/blob/master/LICENSE)

@ -0,0 +1,26 @@
package rel
import (
"context"
)
// Adapter interface
type Adapter interface {
Close() error
Instrumentation(instrumenter Instrumenter)
Ping(ctx context.Context) error
Aggregate(ctx context.Context, query Query, mode string, field string) (int, error)
Query(ctx context.Context, query Query) (Cursor, error)
Insert(ctx context.Context, query Query, primaryField string, mutates map[string]Mutate, onConflict OnConflict) (interface{}, error)
InsertAll(ctx context.Context, query Query, primaryField string, fields []string, bulkMutates []map[string]Mutate, onConflict OnConflict) ([]interface{}, error)
Update(ctx context.Context, query Query, primaryField string, mutates map[string]Mutate) (int, error)
Delete(ctx context.Context, query Query) (int, error)
Exec(ctx context.Context, stmt string, args []interface{}) (int64, int64, error)
Begin(ctx context.Context) (Adapter, error)
Commit(ctx context.Context) error
Rollback(ctx context.Context) error
Apply(ctx context.Context, migration Migration) error
}

@ -0,0 +1,150 @@
package rel
import (
"reflect"
)
// Association provides abstraction to work with association of document or collection.
type Association struct {
meta AssociationMeta
rv reflect.Value
}
// Type of association.
func (a Association) Type() AssociationType {
return a.meta.Type()
}
// Document returns association target as document.
// If association is zero, second return value will be false.
func (a Association) Document() (*Document, bool) {
return a.document(false)
}
// LazyDocument is a lazy version of Document.
// If rv is a null pointer, it returns a document that delays setting the value of rv
// until Document#Add() is called.
func (a Association) LazyDocument() (*Document, bool) {
return a.document(true)
}
func (a Association) document(lazy bool) (*Document, bool) {
var (
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, !lazy)
)
switch rv.Kind() {
case reflect.Ptr:
if rv.IsNil() {
if !lazy {
rv.Set(reflect.New(rv.Type().Elem()))
}
return NewDocument(rv), false
}
var (
doc = NewDocument(rv)
)
return doc, doc.Persisted()
default:
var (
doc = NewDocument(rv.Addr())
)
return doc, doc.Persisted()
}
}
// Collection returns association target as collection.
// If association is zero, second return value will be false.
func (a Association) Collection() (*Collection, bool) {
var (
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, true)
loaded = !rv.IsNil()
)
if rv.Kind() == reflect.Ptr {
if !loaded {
rv.Set(reflect.New(rv.Type().Elem()))
rv.Elem().Set(reflect.MakeSlice(rv.Elem().Type(), 0, 0))
}
return NewCollection(rv), loaded
}
if !loaded {
rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
}
return NewCollection(rv.Addr()), loaded
}
// IsZero returns true if association is not loaded.
func (a Association) IsZero() bool {
var (
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, false)
)
return isDeepZero(reflect.Indirect(rv), 1)
}
// ReferenceField of the association.
func (a Association) ReferenceField() string {
return a.meta.ReferenceField()
}
// ReferenceValue of the association.
func (a Association) ReferenceValue() interface{} {
return indirectInterface(reflectValueFieldByIndex(a.rv, a.meta.referenceIndex, false))
}
// ForeignField of the association.
func (a Association) ForeignField() string {
return a.meta.ForeignField()
}
// ForeignValue of the association.
// It'll panic if association type is has many.
func (a Association) ForeignValue() interface{} {
if a.Type() == HasMany {
panic("rel: cannot infer foreign value for has many or many to many association")
}
var (
rv = reflectValueFieldByIndex(a.rv, a.meta.targetIndex, false)
)
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
return indirectInterface(reflectValueFieldByIndex(rv, a.meta.foreignIndex, false))
}
// Through return intermediary association.
func (a Association) Through() string {
return a.meta.Through()
}
// Autoload assoc setting when parent is loaded.
func (a Association) Autoload() bool {
return a.meta.Autoload()
}
// Autosave setting when parent is created/updated/deleted.
func (a Association) Autosave() bool {
return a.meta.Autosave()
}
func newAssociation(rv reflect.Value, index []int) Association {
if rv.Kind() == reflect.Ptr {
rv = rv.Elem()
}
return Association{
meta: getAssociationMeta(rv.Type(), index),
rv: rv,
}
}

@ -0,0 +1,182 @@
package rel
import (
"reflect"
"sync"
"github.com/serenize/snaker"
)
var associationMetaCache sync.Map
type associationKey struct {
rt reflect.Type
// string repr of index, because []int is not hashable
index string
}
// AssociationType defines the type of association in database.
type AssociationType uint8
const (
// BelongsTo association.
BelongsTo = iota
// HasOne association.
HasOne
// HasMany association.
HasMany
)
type cachedAssociationMeta struct {
typ AssociationType
targetIndex []int
referenceField string
referenceIndex []int
foreignField string
foreignIndex []int
through string
autoload bool
autosave bool
}
type AssociationMeta struct {
rt reflect.Type
cachedAssociationMeta
}
// Type of association.
func (am AssociationMeta) Type() AssociationType {
return am.typ
}
// ReferenceField of the association.
func (am AssociationMeta) ReferenceField() string {
return am.referenceField
}
// ForeignField of the association.
func (am AssociationMeta) ForeignField() string {
return am.foreignField
}
// Through return intermediary association.
func (am AssociationMeta) Through() string {
return am.through
}
// Autoload assoc setting when parent is loaded.
func (am AssociationMeta) Autoload() bool {
return am.autoload
}
// Autosave setting when parent is created/updated/deleted.
func (am AssociationMeta) Autosave() bool {
return am.autosave
}
// Document returns association target document meta.
func (am AssociationMeta) DocumentMeta() DocumentMeta {
var (
rt = am.rt.FieldByIndex(am.targetIndex).Type
)
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
if rt.Kind() == reflect.Slice {
rt = rt.Elem()
}
return getDocumentMeta(rt, false)
}
func getAssociationMeta(rt reflect.Type, index []int) AssociationMeta {
var (
key = associationKey{
rt: rt,
index: encodeIndices(index),
}
)
if val, cached := associationMetaCache.Load(key); cached {
return AssociationMeta{
rt: rt,
cachedAssociationMeta: val.(cachedAssociationMeta),
}
}
var (
sf = rt.FieldByIndex(index)
ft = sf.Type
ref = sf.Tag.Get("ref")
fk = sf.Tag.Get("fk")
fName, _ = fieldName(sf)
assocMeta = cachedAssociationMeta{
targetIndex: index,
through: sf.Tag.Get("through"),
autoload: sf.Tag.Get("auto") == "true" || sf.Tag.Get("autoload") == "true",
autosave: sf.Tag.Get("auto") == "true" || sf.Tag.Get("autosave") == "true",
}
)
if assocMeta.autosave && assocMeta.through != "" {
panic("rel: autosave is not supported for has one/has many through association")
}
for ft.Kind() == reflect.Ptr || ft.Kind() == reflect.Slice {
ft = ft.Elem()
}
var (
refDocMeta = getDocumentMeta(rt, true)
fkDocMeta = getDocumentMeta(ft, true)
)
// Try to guess ref and fk if not defined.
if ref == "" || fk == "" {
// TODO: replace "id" with inferred primary field
if assocMeta.through != "" {
ref = "id"
fk = "id"
} else if _, isBelongsTo := refDocMeta.index[fName+"_id"]; isBelongsTo {
ref = fName + "_id"
fk = "id"
} else {
ref = "id"
fk = snaker.CamelToSnake(rt.Name()) + "_id"
}
}
if id, exist := refDocMeta.index[ref]; !exist {
panic("rel: references (" + ref + ") field not found ")
} else {
assocMeta.referenceIndex = id
assocMeta.referenceField = ref
}
if id, exist := fkDocMeta.index[fk]; !exist {
panic("rel: foreign_key (" + fk + ") field not found")
} else {
assocMeta.foreignIndex = id
assocMeta.foreignField = fk
}
// guess assoc type
if sf.Type.Kind() == reflect.Slice || (sf.Type.Kind() == reflect.Ptr && sf.Type.Elem().Kind() == reflect.Slice) {
assocMeta.typ = HasMany
} else {
if len(assocMeta.referenceField) > len(assocMeta.foreignField) {
assocMeta.typ = BelongsTo
} else {
assocMeta.typ = HasOne
}
}
associationMetaCache.Store(key, assocMeta)
return AssociationMeta{
rt: rt,
cachedAssociationMeta: assocMeta,
}
}

@ -0,0 +1,320 @@
package rel
import (
"bytes"
"reflect"
"time"
)
type pair = [2]interface{}
// Changeset mutator for structs.
// This allows REL to efficiently to perform update operation only on updated fields and association.
// The catch is, enabling changeset will duplicates the original struct values which consumes more memory.
type Changeset struct {
doc *Document
snapshot []interface{}
assoc map[string]Changeset
assocMany map[string]map[interface{}]Changeset
}
func (c Changeset) valueChanged(typ reflect.Type, old interface{}, new interface{}) bool {
if oeq, ok := old.(interface{ Equal(interface{}) bool }); ok {
return !oeq.Equal(new)
}
if ot, ok := old.(time.Time); ok {
return !ot.Equal(new.(time.Time))
}
if typ.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Uint8 {
return !bytes.Equal(reflect.ValueOf(old).Bytes(), reflect.ValueOf(new).Bytes())
}
return !(typ.Comparable() && old == new)
}
// FieldChanged returns true if field exists and it's already changed.
// returns false otherwise.
func (c Changeset) FieldChanged(field string) bool {
for i, f := range c.doc.Fields() {
if f == field {
var (
typ, _ = c.doc.Type(field)
old = c.snapshot[i]
new, _ = c.doc.Value(field)
)
return c.valueChanged(typ, old, new)
}
}
return false
}
// Changes returns map of changes.
func (c Changeset) Changes() map[string]interface{} {
return buildChanges(c.doc, c)
}
// Apply mutation.
func (c Changeset) Apply(doc *Document, mut *Mutation) {
var (
t = Now()
)
for i, field := range c.doc.Fields() {
var (
typ, _ = c.doc.Type(field)
old = c.snapshot[i]
new, _ = c.doc.Value(field)
)
if c.valueChanged(typ, old, new) {
mut.Add(Set(field, new))
}
}
if !mut.IsMutatesEmpty() && c.doc.Flag(HasUpdatedAt) && c.doc.SetValue("updated_at", t) {
mut.Add(Set("updated_at", t))
}
if mut.Cascade {
for _, field := range doc.BelongsTo() {
c.applyAssoc(field, mut)
}
for _, field := range doc.HasOne() {
c.applyAssoc(field, mut)
}
for _, field := range doc.HasMany() {
c.applyAssocMany(field, mut)
}
}
}
func (c Changeset) applyAssoc(field string, mut *Mutation) {
assoc := c.doc.Association(field)
if assoc.IsZero() {
return
}
doc, _ := assoc.Document()
if ch, ok := c.assoc[field]; ok {
if amod := Apply(doc, ch); !amod.IsEmpty() {
mut.SetAssoc(field, amod)
}
} else {
amod := Apply(doc, newStructset(doc, false))
mut.SetAssoc(field, amod)
}
}
func (c Changeset) applyAssocMany(field string, mut *Mutation) {
if chs, ok := c.assocMany[field]; ok {
var (
assoc = c.doc.Association(field)
col, _ = assoc.Collection()
muts = make([]Mutation, 0, col.Len())
updatedIDs = make(map[interface{}]struct{})
deletedIDs []interface{}
)
for i := 0; i < col.Len(); i++ {
var (
doc = col.Get(i)
pValue = doc.PrimaryValue()
)
if ch, ok := chs[pValue]; ok {
updatedIDs[pValue] = struct{}{}
if amod := Apply(doc, ch); !amod.IsEmpty() {
muts = append(muts, amod)
}
} else {
muts = append(muts, Apply(doc, newStructset(doc, false)))
}
}
// leftover snapshot.
if len(updatedIDs) != len(chs) {
for id := range chs {
if _, ok := updatedIDs[id]; !ok {
deletedIDs = append(deletedIDs, id)
}
}
}
if len(muts) > 0 || len(deletedIDs) > 0 {
mut.SetAssoc(field, muts...)
mut.SetDeletedIDs(field, deletedIDs)
}
} else {
newStructset(c.doc, false).buildAssocMany(field, mut)
}
}
// NewChangeset returns new changeset mutator for given record.
func NewChangeset(record interface{}) Changeset {
return newChangeset(NewDocument(record))
}
func newChangeset(doc *Document) Changeset {
c := Changeset{
doc: doc,
snapshot: make([]interface{}, len(doc.Fields())),
assoc: make(map[string]Changeset),
assocMany: make(map[string]map[interface{}]Changeset),
}
for i, field := range doc.Fields() {
c.snapshot[i], _ = doc.Value(field)
}
for _, field := range doc.BelongsTo() {
initChangesetAssoc(doc, c.assoc, field)
}
for _, field := range doc.HasOne() {
initChangesetAssoc(doc, c.assoc, field)
}
for _, field := range doc.HasMany() {
initChangesetAssocMany(doc, c.assocMany, field)
}
return c
}
func initChangesetAssoc(doc *Document, assoc map[string]Changeset, field string) {
doc, loaded := doc.Association(field).Document()
if !loaded {
return
}
assoc[field] = newChangeset(doc)
}
func initChangesetAssocMany(doc *Document, assoc map[string]map[interface{}]Changeset, field string) {
col, loaded := doc.Association(field).Collection()
if !loaded {
return
}
assoc[field] = make(map[interface{}]Changeset)
for i := 0; i < col.Len(); i++ {
var (
doc = col.Get(i)
pValue = doc.PrimaryValue()
)
if !isZero(pValue) {
assoc[field][pValue] = newChangeset(doc)
}
}
}
func buildChanges(doc *Document, c Changeset) map[string]interface{} {
var (
changes = make(map[string]interface{})
fields []string
)
if doc != nil {
fields = doc.Fields()
} else {
fields = c.doc.Fields()
}
for i, field := range fields {
switch {
case doc == nil:
if old := c.snapshot[i]; old != nil {
changes[field] = pair{old, nil}
}
case i >= len(c.snapshot):
if new, _ := doc.Value(field); new != nil {
changes[field] = pair{nil, new}
}
default:
old := c.snapshot[i]
new, _ := doc.Value(field)
if typ, _ := doc.Type(field); c.valueChanged(typ, old, new) {
changes[field] = pair{old, new}
}
}
}
if doc == nil || len(c.snapshot) == 0 {
return changes
}
for _, field := range doc.BelongsTo() {
buildChangesAssoc(changes, c, field)
}
for _, field := range doc.HasOne() {
buildChangesAssoc(changes, c, field)
}
for _, field := range doc.HasMany() {
buildChangesAssocMany(changes, c, field)
}
return changes
}
func buildChangesAssoc(out map[string]interface{}, c Changeset, field string) {
assoc := c.doc.Association(field)
if assoc.IsZero() {
return
}
doc, _ := assoc.Document()
if changes := buildChanges(doc, c.assoc[field]); len(changes) != 0 {
out[field] = changes
}
}
func buildChangesAssocMany(out map[string]interface{}, c Changeset, field string) {
var (
changes []map[string]interface{}
chs = c.assocMany[field]
assoc = c.doc.Association(field)
col, _ = assoc.Collection()
updatedIDs = make(map[interface{}]struct{})
)
for i := 0; i < col.Len(); i++ {
var (
doc = col.Get(i)
pValue = doc.PrimaryValue()
ch, isUpdate = chs[pValue]
)
if isUpdate {
updatedIDs[pValue] = struct{}{}
}
if dChanges := buildChanges(doc, ch); len(dChanges) != 0 {
changes = append(changes, dChanges)
}
}
// leftover snapshot.
if len(updatedIDs) != len(chs) {
for id, ch := range chs {
if _, ok := updatedIDs[id]; !ok {
changes = append(changes, buildChanges(nil, ch))
}
}
}
if len(changes) != 0 {
out[field] = changes
}
}

@ -0,0 +1,220 @@
package rel
import (
"reflect"
)
type slice interface {
table
Reset()
Add() *Document
Get(index int) *Document
Len() int
Meta() DocumentMeta
}
// Collection provides an abstraction over reflect to easily works with slice for database purpose.
type Collection struct {
v interface{}
rv reflect.Value
rt reflect.Type
meta DocumentMeta
swapper func(i, j int)
}
// ReflectValue of referenced document.
func (c Collection) ReflectValue() reflect.Value {
return c.rv
}
// Table returns name of the table.
func (c *Collection) Table() string {
return c.meta.Table()
}
// PrimaryFields column name of this collection.
func (c Collection) PrimaryFields() []string {
if p, ok := c.v.(primary); ok {
return p.PrimaryFields()
}
if len(c.meta.primaryField) == 0 {
panic("rel: failed to infer primary key for type " + c.rt.String())
}
return c.meta.primaryField
}
// PrimaryField column name of this document.
// panic if document uses composite key.
func (c Collection) PrimaryField() string {
if fields := c.PrimaryFields(); len(fields) == 1 {
return fields[0]
}
panic("rel: composite primary key is not supported")
}
// PrimaryValues of collection.
// Returned value will be interface of slice interface.
func (c Collection) PrimaryValues() []interface{} {
if p, ok := c.v.(primary); ok {
return p.PrimaryValues()
}
var (
index = c.meta.primaryIndex
pValues = make([]interface{}, len(c.PrimaryFields()))
)
if index != nil {
for i := range index {
var (
idxLen = c.rv.Len()
values = make([]interface{}, 0, idxLen)
)
for j := 0; j < idxLen; j++ {
if item := c.rvIndex(j); item.IsValid() {
values = append(values, reflectValueFieldByIndex(item, index[i], false).Interface())
}
}
pValues[i] = values
}
} else {
// using interface.
var (
tmp = make([][]interface{}, len(pValues))
)
for i := 0; i < c.rv.Len(); i++ {
item := c.rvIndex(i)
if !item.IsValid() {
continue
}
for j, id := range item.Interface().(primary).PrimaryValues() {
tmp[j] = append(tmp[j], id)
}
}
for i := range tmp {
pValues[i] = tmp[i]
}
}
return pValues
}
// PrimaryValue of this document.
// panic if document uses composite key.
func (c Collection) PrimaryValue() interface{} {
if values := c.PrimaryValues(); len(values) == 1 {
return values[0]
}
panic("rel: composite primary key is not supported")
}
func (c Collection) rvIndex(index int) reflect.Value {
return reflect.Indirect(c.rv.Index(index))
}
// Get an element from the underlying slice as a document.
func (c Collection) Get(index int) *Document {
return NewDocument(c.rvIndex(index).Addr())
}
// Len of the underlying slice.
func (c Collection) Len() int {
return c.rv.Len()
}
// Meta returns document meta.
func (c Collection) Meta() DocumentMeta {
return c.meta
}
// Reset underlying slice to be zero length.
func (c Collection) Reset() {
c.rv.Set(reflect.MakeSlice(c.rt, 0, 0))
}
// Add new document into collection.
func (c Collection) Add() *Document {
var (
index = c.Len()
typ = c.rt.Elem()
drv = reflect.Zero(typ)
)
if typ.Kind() == reflect.Ptr && drv.IsNil() {
drv = reflect.New(drv.Type().Elem())
}
c.rv.Set(reflect.Append(c.rv, drv))
return NewDocument(c.rvIndex(index).Addr())
}
// Truncate collection.
func (c Collection) Truncate(i, j int) {
c.rv.Set(c.rv.Slice(i, j))
}
// Slice returns a new collection that is a slice of the original collection.s
func (c Collection) Slice(i, j int) *Collection {
return NewCollection(c.rv.Slice(i, j), true)
}
// Swap element in the collection.
func (c Collection) Swap(i, j int) {
if c.swapper == nil {
c.swapper = reflect.Swapper(c.rv.Interface())
}
c.swapper(i, j)
}
// NewCollection used to create abstraction to work with slice.
// COllection can be created using interface or reflect.Value.
func NewCollection(records interface{}, readonly ...bool) *Collection {
switch v := records.(type) {
case *Collection:
return v
case reflect.Value:
return newCollection(v.Interface(), v, len(readonly) > 0 && readonly[0])
case reflect.Type:
panic("rel: cannot use reflect.Type")
case nil:
panic("rel: cannot be nil")
default:
return newCollection(v, reflect.ValueOf(v), len(readonly) > 0 && readonly[0])
}
}
func newCollection(v interface{}, rv reflect.Value, readonly bool) *Collection {
var (
rt = rv.Type()
)
if rt.Kind() != reflect.Ptr {
if !readonly {
panic("rel: must be a pointer to slice")
}
} else {
rv = rv.Elem()
rt = rt.Elem()
}
if rt.Kind() != reflect.Slice {
panic("rel: must be a slice or pointer to a slice")
}
return &Collection{
v: v,
rv: rv,
rt: rt,
meta: getDocumentMeta(indirectReflectType(rt.Elem()), false),
}
}

@ -0,0 +1,86 @@
package rel
// ColumnType definition.
type ColumnType string
const (
// ID ColumnType.
ID ColumnType = "ID"
// BigID ColumnType.
BigID ColumnType = "BigID"
// Bool ColumnType.
Bool ColumnType = "BOOL"
// SmallInt ColumnType.
SmallInt ColumnType = "SMALLINT"
// Int ColumnType.
Int ColumnType = "INT"
// BigInt ColumnType.
BigInt ColumnType = "BIGINT"
// Float ColumnType.
Float ColumnType = "FLOAT"
// Decimal ColumnType.
Decimal ColumnType = "DECIMAL"
// String ColumnType.
String ColumnType = "STRING"
// Text ColumnType.
Text ColumnType = "TEXT"
// JSON ColumnType that will fallback to Text ColumnType if adapter does not support it.
JSON ColumnType = "JSON"
// Date ColumnType.
Date ColumnType = "DATE"
// DateTime ColumnType.
DateTime ColumnType = "DATETIME"
// Time ColumnType.
Time ColumnType = "TIME"
)
// Column definition.
type Column struct {
Op SchemaOp
Name string
Type ColumnType
Rename string
Primary bool
Unique bool
Required bool
Unsigned bool
Limit int
Precision int
Scale int
Default interface{}
Options string
}
func (Column) internalTableDefinition() {}
func createColumn(name string, typ ColumnType, options []ColumnOption) Column {
column := Column{
Op: SchemaCreate,
Name: name,
Type: typ,
}
applyColumnOptions(&column, options)
return column
}
func renameColumn(name string, newName string, options []ColumnOption) Column {
column := Column{
Op: SchemaRename,
Name: name,
Rename: newName,
}
applyColumnOptions(&column, options)
return column
}
func dropColumn(name string, options []ColumnOption) Column {
column := Column{
Op: SchemaDrop,
Name: name,
}
applyColumnOptions(&column, options)
return column
}

@ -0,0 +1,35 @@
package rel
import (
"context"
)
type contextKey int8
type contextWrapper struct {
ctx context.Context
adapter Adapter
}
var ctxKey contextKey
// fetchContext and use adapter passed by context if exists.
// it stores contextData values to struct for fast repeated access.
func fetchContext(ctx context.Context, adapter Adapter) contextWrapper {
if adp, ok := ctx.Value(ctxKey).(Adapter); ok {
adapter = adp
}
return contextWrapper{
ctx: ctx,
adapter: adapter,
}
}
// wrapContext wraps adapter inside context.
func wrapContext(ctx context.Context, adapter Adapter) contextWrapper {
return contextWrapper{
ctx: context.WithValue(ctx, ctxKey, adapter),
adapter: adapter,
}
}

@ -0,0 +1,277 @@
// Modified from: database/sql/convert.go
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
package rel
import (
"database/sql"
"database/sql/driver"
"fmt"
"reflect"
"strconv"
"time"
)
var _, localTimeOffset = time.Now().Local().Zone()
// 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.
// dest will be set to zero value if src is nil.
// this function assumes dest will never be nil.
func convertAssign(dest, src interface{}) error {
// Common cases, without reflect.
switch s := src.(type) {
case string:
switch d := dest.(type) {
case *string:
*d = s
return nil
case *[]byte:
*d = []byte(s)
return nil
case *sql.RawBytes:
*d = append((*d)[:0], s...)
return nil
}
case []byte:
switch d := dest.(type) {
case *string:
*d = string(s)
return nil
case *interface{}:
*d = cloneBytes(s)
return nil
case *[]byte:
*d = cloneBytes(s)
return nil
case *sql.RawBytes:
*d = s
return nil
}
case time.Time:
switch d := dest.(type) {
case *time.Time:
// make sure timezone equal for test assertion.
if _, offset := s.Zone(); offset == localTimeOffset {
*d = s.Local()
} else {
*d = s
}
return nil
case *string:
*d = s.Format(time.RFC3339Nano)
return nil
case *[]byte:
*d = []byte(s.Format(time.RFC3339Nano))
return nil
case *sql.RawBytes:
*d = s.AppendFormat((*d)[:0], time.RFC3339Nano)
return nil
}
case nil:
assignZero(dest)
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:
if s, ok := asString(src); ok {
*d = s
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
}
dpv := reflect.ValueOf(dest)
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.
if s, ok := asString(src); ok {
switch dv.Kind() {
case reflect.Ptr:
dv.Set(reflect.New(dv.Type().Elem()))
return convertAssign(dv.Interface(), src)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits())
if err != nil {
// The errors that ParseInt returns have concrete type *NumError
err = err.(*strconv.NumError).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:
u64, err := strconv.ParseUint(s, 10, dv.Type().Bits())
if err != nil {
// The errors that ParseUint returns have concrete type *NumError
err = err.(*strconv.NumError).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:
f64, err := strconv.ParseFloat(s, dv.Type().Bits())
if err != nil {
// The errors that ParseFloat returns have concrete type *NumError
err = err.(*strconv.NumError).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:
dv.SetString(s)
return nil
}
}
return fmt.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, dest)
}
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, bool) {
switch v := src.(type) {
case string:
return v, true
case []byte:
return string(v), true
}
rv := reflect.ValueOf(src)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(rv.Int(), 10), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(rv.Uint(), 10), true
case reflect.Float64:
return strconv.FormatFloat(rv.Float(), 'g', -1, 64), true
case reflect.Float32:
return strconv.FormatFloat(rv.Float(), 'g', -1, 32), true
case reflect.Bool:
return strconv.FormatBool(rv.Bool()), true
}
return "", false
}
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
}
func assignZero(dest interface{}) {
switch d := dest.(type) {
case *bool:
*d = false
case *string:
*d = ""
case *int:
*d = 0
case *int8:
*d = 0
case *int16:
*d = 0
case *int32:
*d = 0
case *int64:
*d = 0
case *uint:
*d = 0
case *uint8:
*d = 0
case *uint16:
*d = 0
case *uint32:
*d = 0
case *uint64:
*d = 0
case *uintptr:
*d = 0
case *float32:
*d = 0
case *float64:
*d = 0
case *interface{}:
*d = nil
case *[]byte:
*d = nil
case *sql.RawBytes:
*d = nil
default:
rv := reflect.ValueOf(dest)
rv.Elem().Set(reflect.Zero(rv.Type().Elem()))
}
}

@ -0,0 +1,111 @@
package rel
import (
"database/sql"
"reflect"
)
// Cursor is interface to work with database result (used by adapter).
type Cursor interface {
Close() error
Fields() ([]string, error)
Next() bool
Scan(...interface{}) error
NopScanner() interface{} // TODO: conflict with manual scanners interface
}
func scanOne(cur Cursor, doc *Document) error {
defer cur.Close()
fields, err := cur.Fields()
if err != nil {
return err
}
if !cur.Next() {
return NotFoundError{}
}
var (
scanners = doc.Scanners(fields)
)
return cur.Scan(scanners...)
}
func scanAll(cur Cursor, col *Collection) error {
defer cur.Close()
fields, err := cur.Fields()
if err != nil {
return err
}
for cur.Next() {
var (
doc = col.Add()
scanners = doc.Scanners(fields)
)
if err := cur.Scan(scanners...); err != nil {
return err
}
}
return nil
}
func scanMulti(cur Cursor, keyField string, keyType reflect.Type, cols map[interface{}][]slice) error {
defer cur.Close()
fields, err := cur.Fields()
if err != nil {
return err
}
var (
found = false
keyValue = reflect.New(keyType)
keyScanners = make([]interface{}, len(fields))
)
for i, field := range fields {
if keyField == field {
found = true
keyScanners[i] = keyValue.Interface()
} else {
// need to create distinct copies
// otherwise next scan result will be corrupted
keyScanners[i] = &sql.RawBytes{}
}
}
if !found && fields != nil {
panic("rel: primary key row does not exists")
}
// scan the result
for cur.Next() {
// scan key
if err := cur.Scan(keyScanners...); err != nil {
return err
}
var (
key = reflect.Indirect(keyValue).Interface()
)
for _, col := range cols[key] {
var (
doc = col.Add()
scanners = doc.Scanners(fields)
)
if err := cur.Scan(scanners...); err != nil {
return err
}
}
}
return nil
}

@ -0,0 +1,338 @@
package rel
import (
"database/sql"
"reflect"
"strings"
)
// Document provides an abstraction over reflect to easily works with struct for database purpose.
type Document struct {
v interface{}
rv reflect.Value
rt reflect.Type
meta DocumentMeta
}
// ReflectValue of referenced document.
func (d Document) ReflectValue() reflect.Value {
return d.rv
}
// Table returns name of the table.
func (d Document) Table() string {
// TODO: handle anonymous struct
return d.meta.Table()
}
// PrimaryFields column name of this document.
func (d Document) PrimaryFields() []string {
return d.meta.PrimaryFields()
}
// PrimaryField column name of this document.
// panic if document uses composite key.
func (d Document) PrimaryField() string {
return d.meta.PrimaryField()
}
// PrimaryValues of this document.
func (d Document) PrimaryValues() []interface{} {
if p, ok := d.v.(primary); ok {
return p.PrimaryValues()
}
if len(d.meta.primaryIndex) == 0 {
panic("rel: failed to infer primary key for type " + d.rt.String())
}
var (
pValues = make([]interface{}, len(d.meta.primaryIndex))
)
for i := range pValues {
pValues[i] = reflectValueFieldByIndex(d.rv, d.meta.primaryIndex[i], false).Interface()
}
return pValues
}
// PrimaryValue of this document.
// panic if document uses composite key.
func (d Document) PrimaryValue() interface{} {
if values := d.PrimaryValues(); len(values) == 1 {
return values[0]
}
panic("rel: composite primary key is not supported")
}
// Persisted returns true if document primary key is not zero.
func (d Document) Persisted() bool {
var (
pValues = d.PrimaryValues()
)
for i := range pValues {
if !isZero(pValues[i]) {
return true
}
}
return false
}
// Index returns map of column name and it's struct index.
func (d Document) Index() map[string][]int {
return d.meta.Index()
}
// Fields returns list of fields available on this document.
func (d Document) Fields() []string {
return d.meta.Fields()
}
// Type returns reflect.Type of given field. if field does not exist, second returns value will be false.
func (d Document) Type(field string) (reflect.Type, bool) {
return d.meta.Type(field)
}
// Value returns value of given field. if field does not exist, second returns value will be false.
func (d Document) Value(field string) (interface{}, bool) {
if i, ok := d.meta.index[field]; ok {
var (
value interface{}
fv = reflectValueFieldByIndex(d.rv, i, false)
ft = fv.Type()
)
if ft.Kind() == reflect.Ptr {
if !fv.IsNil() {
value = fv.Elem().Interface()
}
} else {
value = fv.Interface()
}
return value, true
}
return nil, false
}
// SetValue of the field, it returns false if field does not exist, or it's not assignable.
func (d Document) SetValue(field string, value interface{}) bool {
if i, ok := d.meta.index[field]; ok {
var (
rv reflect.Value
rt reflect.Type
fv = reflectValueFieldByIndex(d.rv, i, true)
ft = fv.Type()
)
switch v := value.(type) {
case nil:
rv = reflect.Zero(ft)
case reflect.Value:
rv = reflect.Indirect(v)
default:
rv = reflect.Indirect(reflect.ValueOf(value))
}
rt = rv.Type()
if fv.Type() == rt || rt.AssignableTo(ft) {
fv.Set(rv)
return true
}
if rt.ConvertibleTo(ft) {
return setConvertValue(ft, fv, rt, rv)
}
if ft.Kind() == reflect.Ptr {
return setPointerValue(ft, fv, rt, rv)
}
}
return false
}
// Scanners returns slice of sql.Scanner for given fields.
func (d Document) Scanners(fields []string) []interface{} {
var (
result = make([]interface{}, len(fields))
assocRefs map[string]struct {
fields []string
indexes []int
}
)
for index, field := range fields {
if structIndex, ok := d.meta.index[field]; ok {
var (
fv = reflectValueFieldByIndex(d.rv, structIndex, true)
ft = fv.Type()
)
if ft.Kind() == reflect.Ptr {
result[index] = fv.Addr().Interface()
} else {
result[index] = Nullable(fv.Addr().Interface())
}
} else if split := strings.SplitN(field, ".", 2); len(split) == 2 {
if assocRefs == nil {
assocRefs = make(map[string]struct {
fields []string
indexes []int
})
}
refs := assocRefs[split[0]]
refs.fields = append(refs.fields, split[1])
refs.indexes = append(refs.indexes, index)
assocRefs[split[0]] = refs
} else {
result[index] = &sql.RawBytes{}
}
}
// get scanners from associations
for assocName, refs := range assocRefs {
if assoc, ok := d.association(assocName); ok && assoc.Type() == BelongsTo || assoc.Type() == HasOne {
var (
assocDoc, _ = assoc.Document()
assocScanners = assocDoc.Scanners(refs.fields)
)
for i, index := range refs.indexes {
result[index] = assocScanners[i]
}
} else {
for _, index := range refs.indexes {
result[index] = &sql.RawBytes{}
}
}
}
return result
}
// BelongsTo fields of this document.
func (d Document) BelongsTo() []string {
return d.meta.BelongsTo()
}
// HasOne fields of this document.
func (d Document) HasOne() []string {
return d.meta.HasOne()
}
// HasMany fields of this document.
func (d Document) HasMany() []string {
return d.meta.HasMany()
}
// Preload fields of this document.
func (d Document) Preload() []string {
return d.meta.Preload()
}
// Association of this document with given name.
func (d Document) Association(name string) Association {
if assoc, ok := d.association(name); ok {
return assoc
}
panic("rel: no field named (" + name + ") in type " + d.rt.String() + " found ")
}
func (d Document) association(name string) (Association, bool) {
index, ok := d.meta.index[name]
if !ok {
return Association{}, false
}
return newAssociation(d.rv, index), true
}
// Reset this document, this is a noop for compatibility with collection.
func (d Document) Reset() {
}
// Add returns this document.
func (d *Document) Add() *Document {
// if d.rv is a null pointer, set it to a new struct.
if d.rv.Kind() == reflect.Ptr && d.rv.IsNil() {
d.rv.Set(reflect.New(d.rv.Type().Elem()))
d.rv = d.rv.Elem()
}
return d
}
// Get always returns this document, this is a noop for compatibility with collection.
func (d *Document) Get(index int) *Document {
return d
}
// Len always returns 1 for document, this is a noop for compatibility with collection.
func (d *Document) Len() int {
return 1
}
// Meta returns document meta.
func (d Document) Meta() DocumentMeta {
return d.meta
}
// Flag returns true if struct contains specified flag.
func (d Document) Flag(flag DocumentFlag) bool {
return d.meta.Flag(flag)
}
// NewDocument used to create abstraction to work with struct.
// Document can be created using interface or reflect.Value.
func NewDocument(record interface{}, readonly ...bool) *Document {
switch v := record.(type) {
case *Document:
return v
case reflect.Value:
return newDocument(v.Interface(), v, len(readonly) > 0 && readonly[0])
case reflect.Type:
panic("rel: cannot use reflect.Type")
case nil:
panic("rel: cannot be nil")
default:
return newDocument(v, reflect.ValueOf(v), len(readonly) > 0 && readonly[0])
}
}
func newDocument(v interface{}, rv reflect.Value, readonly bool) *Document {
var (
rt = rv.Type()
)
if rt.Kind() != reflect.Ptr {
if !readonly {
panic("rel: must be a pointer to struct")
}
} else {
if !rv.IsNil() {
rv = rv.Elem()
}
rt = rt.Elem()
}
if rt.Kind() != reflect.Struct {
panic("rel: must be a struct or pointer to a struct")
}
return &Document{
v: v,
rv: rv,
rt: rt,
meta: getDocumentMeta(rt, false),
}
}

@ -0,0 +1,431 @@
package rel
import (
"reflect"
"strings"
"sync"
"time"
"github.com/jinzhu/inflection"
"github.com/serenize/snaker"
)
var (
primariesCache sync.Map
documentMetaCache sync.Map
rtTime = reflect.TypeOf(time.Time{})
rtBool = reflect.TypeOf(false)
rtInt = reflect.TypeOf(int(0))
rtTable = reflect.TypeOf((*table)(nil)).Elem()
rtPrimary = reflect.TypeOf((*primary)(nil)).Elem()
)
// DocumentFlag stores information about document as a flag.
type DocumentFlag int8
// Is returns true if it's defined.
func (df DocumentFlag) Is(flag DocumentFlag) bool {
return (df & flag) == flag
}
const (
// Invalid flag.
Invalid DocumentFlag = 1 << iota
// HasCreatedAt flag.
HasCreatedAt
// HasUpdatedAt flag.
HasUpdatedAt
// HasDeletedAt flag.
HasDeletedAt
// HasDeleted flag.
HasDeleted
// Versioning
HasVersioning
)
type table interface {
Table() string
}
type primary interface {
PrimaryFields() []string
PrimaryValues() []interface{}
}
type primaryData struct {
field []string
index [][]int
}
type cachedDocumentMeta struct {
table string
index map[string][]int
fields []string
belongsTo []string
hasOne []string
hasMany []string
primaryField []string
primaryIndex [][]int
preload []string
flag DocumentFlag
}
// Adds a prefix to field names
func appendWithPrefix(target, fieldNames []string, prefix string) []string {
if prefix == "" {
return append(target, fieldNames...)
}
for _, name := range fieldNames {
target = append(target, prefix+name)
}
return target
}
// Adds a field index and checks for conflicts
func (cdm *cachedDocumentMeta) addFieldIndex(name string, index []int) {
if _, ok := cdm.index[name]; ok {
panic("rel: conflicting field (" + name + ") in struct")
}
cdm.index[name] = index
}
// Transfer values from other document data
func (cdm *cachedDocumentMeta) mergeEmbedded(other cachedDocumentMeta, indexPrefix int, namePrefix string) {
for name, path := range other.index {
cdm.addFieldIndex(namePrefix+name, append([]int{indexPrefix}, path...))
}
cdm.fields = appendWithPrefix(cdm.fields, other.fields, namePrefix)
cdm.belongsTo = appendWithPrefix(cdm.belongsTo, other.belongsTo, namePrefix)
cdm.hasOne = appendWithPrefix(cdm.hasOne, other.hasOne, namePrefix)
cdm.hasMany = appendWithPrefix(cdm.hasMany, other.hasMany, namePrefix)
cdm.primaryField = appendWithPrefix(cdm.primaryField, other.primaryField, namePrefix)
for index := range other.primaryIndex {
cdm.primaryIndex = append(cdm.primaryIndex, append([]int{indexPrefix}, index))
}
cdm.preload = appendWithPrefix(cdm.preload, other.preload, namePrefix)
cdm.flag |= other.flag
}
type DocumentMeta struct {
rt reflect.Type
cachedDocumentMeta
}
// Table returns name of the table.
func (dm DocumentMeta) Table() string {
return dm.table
}
// PrimaryFields column name of this document.
func (dm DocumentMeta) PrimaryFields() []string {
if len(dm.primaryField) == 0 {
panic("rel: failed to infer primary key for type " + dm.rt.String())
}
return dm.primaryField
}
// PrimaryField column name of this document.
// panic if document uses composite key.
func (dm DocumentMeta) PrimaryField() string {
if fields := dm.PrimaryFields(); len(fields) == 1 {
return fields[0]
}
panic("rel: composite primary key is not supported")
}
// Index returns map of column name and it's struct index.
func (dm DocumentMeta) Index() map[string][]int {
return dm.index
}
// Fields returns list of fields available on this document.
func (dm DocumentMeta) Fields() []string {
return dm.fields
}
// Type returns reflect.Type of given field. if field does not exist, second returns value will be false.
func (dm DocumentMeta) Type(field string) (reflect.Type, bool) {
if i, ok := dm.index[field]; ok {
var (
ft = dm.rt.FieldByIndex(i).Type
)
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
} else if ft.Kind() == reflect.Slice && ft.Elem().Kind() == reflect.Ptr {
ft = reflect.SliceOf(ft.Elem().Elem())
}
return ft, true
}
return nil, false
}
// BelongsTo fields of this document.
func (dm DocumentMeta) BelongsTo() []string {
return dm.belongsTo
}
// HasOne fields of this document.
func (dm DocumentMeta) HasOne() []string {
return dm.hasOne
}
// HasMany fields of this document.
func (dm DocumentMeta) HasMany() []string {
return dm.hasMany
}
// Preload fields of this document.
func (dm DocumentMeta) Preload() []string {
return dm.preload
}
// Association of this document with given name.
func (dm DocumentMeta) Association(name string) AssociationMeta {
if assoc, ok := dm.association(name); ok {
return assoc
}
panic("rel: no field named (" + name + ") in type " + dm.rt.String() + " found ")
}
func (dm DocumentMeta) association(name string) (AssociationMeta, bool) {
index, ok := dm.index[name]
if !ok {
return AssociationMeta{}, false
}
return getAssociationMeta(dm.rt, index), true
}
// Flag returns true if struct contains specified flag.
func (dm DocumentMeta) Flag(flag DocumentFlag) bool {
return dm.flag.Is(flag)
}
func getDocumentMeta(rt reflect.Type, skipAssoc bool) DocumentMeta {
if meta, cached := documentMetaCache.Load(rt); cached {
return DocumentMeta{
cachedDocumentMeta: meta.(cachedDocumentMeta),
rt: rt,
}
}
var (
meta = cachedDocumentMeta{
table: tableName(rt),
index: make(map[string][]int, rt.NumField()),
}
)
// TODO probably better to use slice index instead.
for i := 0; i < rt.NumField(); i++ {
var (
sf = rt.Field(i)
typ = sf.Type
name, tagged = fieldName(sf)
)
if c := sf.Name[0]; c < 'A' || c > 'Z' || name == "" {
continue
}
for typ.Kind() == reflect.Ptr || typ.Kind() == reflect.Interface || typ.Kind() == reflect.Slice {
typ = typ.Elem()
}
if typ.Kind() == reflect.Struct && isEmbedded(sf) {
embedded := getDocumentMeta(typ, skipAssoc)
embeddedName := ""
if tagged {
embeddedName = name
}
meta.mergeEmbedded(embedded.cachedDocumentMeta, i, embeddedName)
continue
}
meta.addFieldIndex(name, sf.Index)
if flag := extractFlag(typ, name); flag != Invalid {
meta.fields = append(meta.fields, name)
meta.flag |= flag
continue
}
if typ.Kind() != reflect.Struct {
meta.fields = append(meta.fields, name)
continue
}
// struct without primary key is a field
// TODO: test by scanner/valuer instead?
if pk, _ := searchPrimary(typ); len(pk) == 0 {
meta.fields = append(meta.fields, name)
continue
}
if !skipAssoc {
var (
assocMeta = getAssociationMeta(rt, sf.Index)
)
switch assocMeta.typ {
case BelongsTo:
meta.belongsTo = append(meta.belongsTo, name)
case HasOne:
meta.hasOne = append(meta.hasOne, name)
case HasMany:
meta.hasMany = append(meta.hasMany, name)
}
if assocMeta.autoload {
meta.preload = append(meta.preload, name)
}
}
}
primaryField, primaryIndex := searchPrimary(rt)
meta.primaryField = append(meta.primaryField, primaryField...)
meta.primaryIndex = append(meta.primaryIndex, primaryIndex...)
if !skipAssoc {
documentMetaCache.Store(rt, meta)
}
return DocumentMeta{
rt: rt,
cachedDocumentMeta: meta,
}
}
func extractTimeFlag(name string) DocumentFlag {
switch name {
case "created_at", "inserted_at":
return HasCreatedAt
case "updated_at":
return HasUpdatedAt
case "deleted_at":
return HasDeletedAt
}
return Invalid
}
func extractBoolFlag(name string) DocumentFlag {
if name == "deleted" {
return HasDeleted
}
return Invalid
}
func extractIntFlag(name string) DocumentFlag {
if name == "lock_version" {
return HasVersioning
}
return Invalid
}
func extractFlag(rt reflect.Type, name string) DocumentFlag {
if rt == rtTime {
return extractTimeFlag(name)
}
if rt == rtBool {
return extractBoolFlag(name)
}
if rt == rtInt {
return extractIntFlag(name)
}
return Invalid
}
func fieldName(sf reflect.StructField) (string, bool) {
if tag := sf.Tag.Get("db"); tag != "" {
name := strings.Split(tag, ",")[0]
if name == "-" {
return "", true
}
if name != "" {
return name, true
}
}
return snaker.CamelToSnake(sf.Name), false
}
func isEmbedded(sf reflect.StructField) bool {
// anonymous structs are always embedded
if sf.Anonymous {
return true
}
if tag := sf.Tag.Get("db"); strings.HasSuffix(tag, ",embedded") {
return true
}
return false
}
func searchPrimary(rt reflect.Type) ([]string, [][]int) {
if result, cached := primariesCache.Load(rt); cached {
p := result.(primaryData)
return p.field, p.index
}
var (
field []string
index [][]int
fallbackIndex = -1
)
if rt.Implements(rtPrimary) {
var (
v = reflect.Zero(rt).Interface().(primary)
)
field = v.PrimaryFields()
// index kept nil to mark interface usage
} else {
for i := 0; i < rt.NumField(); i++ {
sf := rt.Field(i)
if tag := sf.Tag.Get("db"); strings.HasSuffix(tag, ",primary") {
index = append(index, sf.Index)
name, _ := fieldName(sf)
field = append(field, name)
continue
}
// check fallback for id field
if strings.EqualFold("id", sf.Name) {
fallbackIndex = i
}
}
}
if len(field) == 0 && fallbackIndex >= 0 {
field = []string{"id"}
index = [][]int{{fallbackIndex}}
}
primariesCache.Store(rt, primaryData{
field: field,
index: index,
})
return field, index
}
func tableName(rt reflect.Type) string {
var name string
if rt.Implements(rtTable) {
name = reflect.Zero(rt).Interface().(table).Table()
} else {
name = inflection.Plural(rt.Name())
name = snaker.CamelToSnake(name)
}
return name
}

@ -0,0 +1,108 @@
package rel
import (
"database/sql"
"errors"
)
var (
// ErrNotFound returned when records not found.
ErrNotFound = NotFoundError{}
// ErrCheckConstraint is an auxiliary variable for error handling.
// This is only to be used when checking error with errors.Is(err, ErrCheckConstraint).
ErrCheckConstraint = ConstraintError{Type: CheckConstraint}
// ErrNotNullConstraint is an auxiliary variable for error handling.
// This is only to be used when checking error with errors.Is(err, ErrNotNullConstraint).
ErrNotNullConstraint = ConstraintError{Type: NotNullConstraint}
// ErrUniqueConstraint is an auxiliary variable for error handling.
// This is only to be used when checking error with errors.Is(err, ErrUniqueConstraint).
ErrUniqueConstraint = ConstraintError{Type: UniqueConstraint}
// ErrPrimaryKeyConstraint is an auxiliary variable for error handling.
// This is only to be used when checking error with errors.Is(err, ErrPrimaryKeyConstraint).
ErrPrimaryKeyConstraint = ConstraintError{Type: PrimaryKeyConstraint}
// ErrForeignKeyConstraint is an auxiliary variable for error handling.
// This is only to be used when checking error with errors.Is(err, ErrForeignKeyConstraint).
ErrForeignKeyConstraint = ConstraintError{Type: ForeignKeyConstraint}
)
// NotFoundError returned whenever Find returns no result.
type NotFoundError struct{}
// Error message.
func (nfe NotFoundError) Error() string {
return "Record not found"
}
// Is returns true when target error is sql.ErrNoRows.
func (nfe NotFoundError) Is(target error) bool {
return errors.Is(target, sql.ErrNoRows)
}
// ConstraintType defines the type of constraint error.
type ConstraintType int8
const (
// CheckConstraint error type.
CheckConstraint ConstraintType = iota
// NotNullConstraint error type.1
NotNullConstraint
// UniqueConstraint error type.1
UniqueConstraint
// PrimaryKeyConstraint error type.1
PrimaryKeyConstraint
// ForeignKeyConstraint error type.1
ForeignKeyConstraint
)
// String representation of the constraint type.
func (ct ConstraintType) String() string {
switch ct {
case CheckConstraint:
return "CheckConstraint"
case NotNullConstraint:
return "NotNullConstraint"
case UniqueConstraint:
return "UniqueConstraint"
case PrimaryKeyConstraint:
return "PrimaryKeyConstraint"
case ForeignKeyConstraint:
return "ForeignKeyConstraint"
default:
return ""
}
}
// ConstraintError returned whenever constraint error encountered.
type ConstraintError struct {
Key string
Type ConstraintType
Err error
}
// Is returns true when target error have the same type and key if defined.
func (ce ConstraintError) Is(target error) bool {
if err, ok := target.(ConstraintError); ok {
return ce.Type == err.Type && (ce.Key == "" || err.Key == "" || ce.Key == err.Key)
}
return false
}
// Unwrap internal error returned by database driver.
func (ce ConstraintError) Unwrap() error {
return ce.Err
}
// Error message.
func (ce ConstraintError) Error() string {
if ce.Err != nil {
return ce.Type.String() + "Error: " + ce.Err.Error()
}
return ce.Type.String() + "Error"
}

@ -0,0 +1,679 @@
package rel
import (
"errors"
"strings"
)
// FilterOp defines enumeration of all supported filter types.
type FilterOp int
func (fo FilterOp) String() string {
return [...]string{
"And",
"Or",
"Not",
"Eq",
"Ne",
"Lt",
"Lte",
"Gt",
"Gte",
"Nil",
"NotNil",
"In",
"Nin",
"Like",
"NotLike",
"Fragment",
}[fo]
}
const (
// FilterAndOp is filter type for and operator.
FilterAndOp FilterOp = iota
// FilterOrOp is filter type for or operator.
FilterOrOp
// FilterNotOp is filter type for not operator.
FilterNotOp
// FilterEqOp is filter type for equal comparison.
FilterEqOp
// FilterNeOp is filter type for not equal comparison.
FilterNeOp
// FilterLtOp is filter type for less than comparison.
FilterLtOp
// FilterLteOp is filter type for less than or equal comparison.
FilterLteOp
// FilterGtOp is filter type for greater than comparison.
FilterGtOp
// FilterGteOp is filter type for greter than or equal comparison.
FilterGteOp
// FilterNilOp is filter type for nil check.
FilterNilOp
// FilterNotNilOp is filter type for not nil check.
FilterNotNilOp
// FilterInOp is filter type for inclusion comparison.
FilterInOp
// FilterNinOp is filter type for not inclusion comparison.
FilterNinOp
// FilterLikeOp is filter type for like comparison.
FilterLikeOp
// FilterNotLikeOp is filter type for not like comparison.
FilterNotLikeOp
// FilterFragmentOp is filter type for custom filter.
FilterFragmentOp
)
// FilterQuery defines details of a condition type.
type FilterQuery struct {
Type FilterOp
Field string
Value interface{}
Inner []FilterQuery
}
// Build Filter query.
func (fq FilterQuery) Build(query *Query) {
query.WhereQuery = query.WhereQuery.And(fq)
}
// None returns true if no filter is specified.
func (fq FilterQuery) None() bool {
return (fq.Type == FilterAndOp ||
fq.Type == FilterOrOp ||
fq.Type == FilterNotOp) &&
len(fq.Inner) == 0
}
func (fq FilterQuery) String() string {
if fq.None() {
return ""
}
var builder strings.Builder
builder.WriteString("where.")
builder.WriteString(fq.Type.String())
builder.WriteByte('(')
switch fq.Type {
case FilterAndOp, FilterOrOp, FilterNotOp:
for i := range fq.Inner {
if i > 0 {
builder.WriteString(", ")
}
builder.WriteString(fq.Inner[i].String())
}
case FilterEqOp, FilterNeOp, FilterLtOp, FilterLteOp, FilterGtOp, FilterGteOp:
builder.WriteByte('"')
builder.WriteString(fq.Field)
builder.WriteString("\", ")
builder.WriteString(fmtiface(fq.Value))
case FilterNilOp, FilterNotNilOp, FilterLikeOp, FilterNotLikeOp:
builder.WriteByte('"')
builder.WriteString(fq.Field)
builder.WriteByte('"')
case FilterInOp, FilterNinOp:
builder.WriteByte('"')
builder.WriteString(fq.Field)
builder.WriteString("\", ")
builder.WriteString(fmtifaces(fq.Value.([]interface{})))
case FilterFragmentOp:
v := fq.Value.([]interface{})
builder.WriteByte('"')
builder.WriteString(fq.Field)
builder.WriteByte('"')
if len(v) > 0 {
builder.WriteString(", ")
builder.WriteString(fmtifaces(v))
}
}
builder.WriteByte(')')
return builder.String()
}
// And wraps filters using and.
func (fq FilterQuery) And(filters ...FilterQuery) FilterQuery {
if fq.None() && len(filters) == 1 {
return filters[0]
} else if fq.Type == FilterAndOp {
fq.Inner = append(fq.Inner, filters...)
return fq
}
inner := append([]FilterQuery{fq}, filters...)
return And(inner...)
}
// Or wraps filters using or.
func (fq FilterQuery) Or(filter ...FilterQuery) FilterQuery {
if fq.None() && len(filter) == 1 {
return filter[0]
} else if fq.Type == FilterOrOp || fq.None() {
fq.Type = FilterOrOp
fq.Inner = append(fq.Inner, filter...)
return fq
}
inner := append([]FilterQuery{fq}, filter...)
return Or(inner...)
}
func (fq FilterQuery) and(other FilterQuery) FilterQuery {
if fq.Type == FilterAndOp {
fq.Inner = append(fq.Inner, other)
return fq
}
return And(fq, other)
}
func (fq FilterQuery) or(other FilterQuery) FilterQuery {
if fq.Type == FilterOrOp || fq.None() {
fq.Type = FilterOrOp
fq.Inner = append(fq.Inner, other)
return fq
}
return Or(fq, other)
}
func (fq FilterQuery) applyIndex(index *Index) {
index.Filter = fq
}
// AndEq append equal expression using and.
func (fq FilterQuery) AndEq(field string, value interface{}) FilterQuery {
return fq.and(Eq(field, value))
}
// AndNe append not equal expression using and.
func (fq FilterQuery) AndNe(field string, value interface{}) FilterQuery {
return fq.and(Ne(field, value))
}
// AndLt append lesser than expression using and.
func (fq FilterQuery) AndLt(field string, value interface{}) FilterQuery {
return fq.and(Lt(field, value))
}
// AndLte append lesser than or equal expression using and.
func (fq FilterQuery) AndLte(field string, value interface{}) FilterQuery {
return fq.and(Lte(field, value))
}
// AndGt append greater than expression using and.
func (fq FilterQuery) AndGt(field string, value interface{}) FilterQuery {
return fq.and(Gt(field, value))
}
// AndGte append greater than or equal expression using and.
func (fq FilterQuery) AndGte(field string, value interface{}) FilterQuery {
return fq.and(Gte(field, value))
}
// AndNil append is nil expression using and.
func (fq FilterQuery) AndNil(field string) FilterQuery {
return fq.and(Nil(field))
}
// AndNotNil append is not nil expression using and.
func (fq FilterQuery) AndNotNil(field string) FilterQuery {
return fq.and(NotNil(field))
}
// AndIn append is in expression using and.
func (fq FilterQuery) AndIn(field string, values ...interface{}) FilterQuery {
return fq.and(In(field, values...))
}
// AndNin append is not in expression using and.
func (fq FilterQuery) AndNin(field string, values ...interface{}) FilterQuery {
return fq.and(Nin(field, values...))
}
// AndLike append like expression using and.
func (fq FilterQuery) AndLike(field string, pattern string) FilterQuery {
return fq.and(Like(field, pattern))
}
// AndNotLike append not like expression using and.
func (fq FilterQuery) AndNotLike(field string, pattern string) FilterQuery {
return fq.and(NotLike(field, pattern))
}
// AndFragment append fragment using and.
func (fq FilterQuery) AndFragment(expr string, values ...interface{}) FilterQuery {
return fq.and(FilterFragment(expr, values...))
}
// OrEq append equal expression using or.
func (fq FilterQuery) OrEq(field string, value interface{}) FilterQuery {
return fq.or(Eq(field, value))
}
// OrNe append not equal expression using or.
func (fq FilterQuery) OrNe(field string, value interface{}) FilterQuery {
return fq.or(Ne(field, value))
}
// OrLt append lesser than expression using or.
func (fq FilterQuery) OrLt(field string, value interface{}) FilterQuery {
return fq.or(Lt(field, value))
}
// OrLte append lesser than or equal expression using or.
func (fq FilterQuery) OrLte(field string, value interface{}) FilterQuery {
return fq.or(Lte(field, value))
}
// OrGt append greater than expression using or.
func (fq FilterQuery) OrGt(field string, value interface{}) FilterQuery {
return fq.or(Gt(field, value))
}
// OrGte append greater than or equal expression using or.
func (fq FilterQuery) OrGte(field string, value interface{}) FilterQuery {
return fq.or(Gte(field, value))
}
// OrNil append is nil expression using or.
func (fq FilterQuery) OrNil(field string) FilterQuery {
return fq.or(Nil(field))
}
// OrNotNil append is not nil expression using or.
func (fq FilterQuery) OrNotNil(field string) FilterQuery {
return fq.or(NotNil(field))
}
// OrIn append is in expression using or.
func (fq FilterQuery) OrIn(field string, values ...interface{}) FilterQuery {
return fq.or(In(field, values...))
}
// OrNin append is not in expression using or.
func (fq FilterQuery) OrNin(field string, values ...interface{}) FilterQuery {
return fq.or(Nin(field, values...))
}
// OrLike append like expression using or.
func (fq FilterQuery) OrLike(field string, pattern string) FilterQuery {
return fq.or(Like(field, pattern))
}
// OrNotLike append not like expression using or.
func (fq FilterQuery) OrNotLike(field string, pattern string) FilterQuery {
return fq.or(NotLike(field, pattern))
}
// OrFragment append fragment using or.
func (fq FilterQuery) OrFragment(expr string, values ...interface{}) FilterQuery {
return fq.or(FilterFragment(expr, values...))
}
// And compares other filters using and.
func And(inner ...FilterQuery) FilterQuery {
if len(inner) == 1 {
return inner[0]
}
return FilterQuery{
Type: FilterAndOp,
Inner: inner,
}
}
// Or compares other filters using or.
func Or(inner ...FilterQuery) FilterQuery {
if len(inner) == 1 {
return inner[0]
}
return FilterQuery{
Type: FilterOrOp,
Inner: inner,
}
}
// Not wraps filters using not.
// It'll negate the filter type if possible.
func Not(inner ...FilterQuery) FilterQuery {
if len(inner) == 1 {
fq := inner[0]
switch fq.Type {
case FilterEqOp:
fq.Type = FilterNeOp
return fq
case FilterLtOp:
fq.Type = FilterGteOp
case FilterLteOp:
fq.Type = FilterGtOp
case FilterGtOp:
fq.Type = FilterLteOp
case FilterGteOp:
fq.Type = FilterLtOp
case FilterNilOp:
fq.Type = FilterNotNilOp
case FilterInOp:
fq.Type = FilterNinOp
case FilterLikeOp:
fq.Type = FilterNotLikeOp
default:
return FilterQuery{
Type: FilterNotOp,
Inner: inner,
}
}
return fq
}
return FilterQuery{
Type: FilterNotOp,
Inner: inner,
}
}
// Eq expression field equal to value.
func Eq(field string, value interface{}) FilterQuery {
return FilterQuery{
Type: FilterEqOp,
Field: field,
Value: value,
}
}
func lockVersion(version int) FilterQuery {
return Eq("lock_version", version)
}
// Ne compares that left value is not equal to right value.
func Ne(field string, value interface{}) FilterQuery {
return FilterQuery{
Type: FilterNeOp,
Field: field,
Value: value,
}
}
// Lt compares that left value is less than to right value.
func Lt(field string, value interface{}) FilterQuery {
return FilterQuery{
Type: FilterLtOp,
Field: field,
Value: value,
}
}
// Lte compares that left value is less than or equal to right value.
func Lte(field string, value interface{}) FilterQuery {
return FilterQuery{
Type: FilterLteOp,
Field: field,
Value: value,
}
}
// Gt compares that left value is greater than to right value.
func Gt(field string, value interface{}) FilterQuery {
return FilterQuery{
Type: FilterGtOp,
Field: field,
Value: value,
}
}
// Gte compares that left value is greater than or equal to right value.
func Gte(field string, value interface{}) FilterQuery {
return FilterQuery{
Type: FilterGteOp,
Field: field,
Value: value,
}
}
// Nil check whether field is nil.
func Nil(field string) FilterQuery {
return FilterQuery{
Type: FilterNilOp,
Field: field,
}
}
// NotNil check whether field is not nil.
func NotNil(field string) FilterQuery {
return FilterQuery{
Type: FilterNotNilOp,
Field: field,
}
}
// In check whethers value of the field is included in values.
func In(field string, values ...interface{}) FilterQuery {
return FilterQuery{
Type: FilterInOp,
Field: field,
Value: values,
}
}
// InInt check whethers integer values of the field is included.
func InInt(field string, values []int) FilterQuery {
var (
ivalues = make([]interface{}, len(values))
)
for i := range values {
ivalues[i] = values[i]
}
return In(field, ivalues...)
}
// InUint check whethers unsigned integer values of the field is included.
func InUint(field string, values []uint) FilterQuery {
var (
ivalues = make([]interface{}, len(values))
)
for i := range values {
ivalues[i] = values[i]
}
return In(field, ivalues...)
}
// InString check whethers string values of the field is included.
func InString(field string, values []string) FilterQuery {
var (
ivalues = make([]interface{}, len(values))
)
for i := range values {
ivalues[i] = values[i]
}
return In(field, ivalues...)
}
// Nin check whethers value of the field is not included in values.
func Nin(field string, values ...interface{}) FilterQuery {
return FilterQuery{
Type: FilterNinOp,
Field: field,
Value: values,
}
}
// NinInt check whethers integer values of the is not included.
func NinInt(field string, values []int) FilterQuery {
var (
ivalues = make([]interface{}, len(values))
)
for i := range values {
ivalues[i] = values[i]
}
return Nin(field, ivalues...)
}
// NinUint check whethers unsigned integer values of the is not included.
func NinUint(field string, values []uint) FilterQuery {
var (
ivalues = make([]interface{}, len(values))
)
for i := range values {
ivalues[i] = values[i]
}
return Nin(field, ivalues...)
}
// NinString check whethers string values of the is not included.
func NinString(field string, values []string) FilterQuery {
var (
ivalues = make([]interface{}, len(values))
)
for i := range values {
ivalues[i] = values[i]
}
return Nin(field, ivalues...)
}
// Like compares value of field to match string pattern.
func Like(field string, pattern string) FilterQuery {
return FilterQuery{
Type: FilterLikeOp,
Field: field,
Value: pattern,
}
}
// NotLike compares value of field to not match string pattern.
func NotLike(field string, pattern string) FilterQuery {
return FilterQuery{
Type: FilterNotLikeOp,
Field: field,
Value: pattern,
}
}
// FilterFragment add custom filter.
func FilterFragment(expr string, values ...interface{}) FilterQuery {
return FilterQuery{
Type: FilterFragmentOp,
Field: expr,
Value: values,
}
}
func filterDocument(doc *Document) FilterQuery {
var (
pFields = doc.PrimaryFields()
pValues = doc.PrimaryValues()
)
return filterDocumentPrimary(pFields, pValues, FilterEqOp)
}
func filterDocumentPrimary(pFields []string, pValues []interface{}, op FilterOp) FilterQuery {
var filter FilterQuery
for i := range pFields {
filter = filter.And(FilterQuery{
Type: op,
Field: pFields[i],
Value: pValues[i],
})
}
return filter
}
func filterCollection(col *Collection) FilterQuery {
var (
pFields = col.PrimaryFields()
pValues = col.PrimaryValues()
length = col.Len()
)
return filterCollectionPrimary(pFields, pValues, length)
}
func filterCollectionPrimary(pFields []string, pValues []interface{}, length int) FilterQuery {
var filter FilterQuery
if len(pFields) == 1 {
filter = In(pFields[0], pValues[0].([]interface{})...)
} else {
var (
andFilters = make([]FilterQuery, length)
)
for i := range pValues {
var (
values = pValues[i].([]interface{})
)
for j := range values {
andFilters[j] = andFilters[j].AndEq(pFields[i], values[j])
}
}
filter = Or(andFilters...)
}
return filter
}
func filterBelongsTo(assoc Association) (FilterQuery, error) {
var (
rValue = assoc.ReferenceValue()
fValue = assoc.ForeignValue()
filter = Eq(assoc.ForeignField(), fValue)
)
if rValue != fValue {
return filter, ConstraintError{
Key: assoc.ReferenceField(),
Type: ForeignKeyConstraint,
Err: errors.New("rel: inconsistent belongs to ref and fk"),
}
}
return filter, nil
}
func filterHasOne(assoc Association, asssocDoc *Document) (FilterQuery, error) {
var (
fField = assoc.ForeignField()
fValue = assoc.ForeignValue()
rValue = assoc.ReferenceValue()
filter = filterDocument(asssocDoc).AndEq(fField, rValue)
)
if rValue != fValue {
return filter, ConstraintError{
Key: fField,
Type: ForeignKeyConstraint,
Err: errors.New("rel: inconsistent has one ref and fk"),
}
}
return filter, nil
}

@ -0,0 +1,41 @@
package rel
// GroupQuery defines group clause of the query.
type GroupQuery struct {
Fields []string
Filter FilterQuery
}
// Build query.
func (gq GroupQuery) Build(query *Query) {
query.GroupQuery = gq
}
// Having appends filter for group query with and operand.
func (gq GroupQuery) Having(filters ...FilterQuery) GroupQuery {
gq.Filter = gq.Filter.And(filters...)
return gq
}
// OrHaving appends filter for group query with or operand.
func (gq GroupQuery) OrHaving(filters ...FilterQuery) GroupQuery {
gq.Filter = gq.Filter.Or(And(filters...))
return gq
}
// Where is alias for having.
func (gq GroupQuery) Where(filters ...FilterQuery) GroupQuery {
return gq.Having(filters...)
}
// OrWhere is alias for OrHaving.
func (gq GroupQuery) OrWhere(filters ...FilterQuery) GroupQuery {
return gq.OrHaving(filters...)
}
// NewGroup query.
func NewGroup(fields ...string) GroupQuery {
return GroupQuery{
Fields: fields,
}
}

@ -0,0 +1,67 @@
package rel
// Index definition.
type Index struct {
Op SchemaOp
Table string
Name string
Unique bool
Columns []string
Optional bool
Filter FilterQuery
Options string
}
func (i Index) description() string {
return i.Op.String() + " index " + i.Name + " on " + i.Table
}
func (Index) internalMigration() {}
func createIndex(table string, name string, columns []string, options []IndexOption) Index {
index := Index{
Op: SchemaCreate,
Table: table,
Name: name,
Columns: columns,
}
applyIndexOptions(&index, options)
return index
}
func createUniqueIndex(table string, name string, columns []string, options []IndexOption) Index {
index := createIndex(table, name, columns, options)
index.Unique = true
return index
}
func dropIndex(table string, name string, options []IndexOption) Index {
index := Index{
Op: SchemaDrop,
Table: table,
Name: name,
}
applyIndexOptions(&index, options)
return index
}
// IndexOption interface.
// Available options are: Comment, Options.
type IndexOption interface {
applyIndex(index *Index)
}
func applyIndexOptions(index *Index, options []IndexOption) {
for i := range options {
options[i].applyIndex(index)
}
}
// Name option for defining custom index name.
type Name string
func (n Name) applyKey(key *Key) {
key.Name = string(n)
}

@ -0,0 +1,40 @@
package rel
import (
"context"
"log"
"strings"
"time"
)
// Instrumenter defines function type that can be used for instrumetation.
// This function should return a function with no argument as a callback for finished execution.
type Instrumenter func(ctx context.Context, op string, message string) func(err error)
// Observe operation.
func (i Instrumenter) Observe(ctx context.Context, op string, message string) func(err error) {
if i != nil {
return i(ctx, op, message)
}
return func(err error) {}
}
// DefaultLogger instrumentation to log queries and rel operation.
func DefaultLogger(ctx context.Context, op string, message string) func(err error) {
// no op for rel functions.
if strings.HasPrefix(op, "rel-") {
return func(error) {}
}
t := time.Now()
return func(err error) {
duration := time.Since(t)
if err != nil {
log.Print("[duration: ", duration, " op: ", op, "] ", message, " - ", err)
} else {
log.Print("[duration: ", duration, " op: ", op, "] ", message)
}
}
}

@ -0,0 +1,168 @@
package rel
import (
"context"
"fmt"
"io"
)
// Iterator allows iterating through all record in database in batch.
type Iterator interface {
io.Closer
Next(record interface{}) error
}
// IteratorOption is used to configure iteration behaviour, such as batch size, start id and finish id.
type IteratorOption interface {
apply(*iterator)
}
type batchSize int
func (bs batchSize) apply(i *iterator) {
i.batchSize = int(bs)
}
// String representation.
func (bs batchSize) String() string {
return fmt.Sprintf("rel.BatchSize(%d)", bs)
}
// BatchSize specifies the size of iterator batch. Defaults to 1000.
func BatchSize(size int) IteratorOption {
return batchSize(size)
}
type start []interface{}
func (s start) apply(i *iterator) {
i.start = s
}
// String representation.
func (s start) String() string {
return fmt.Sprintf("rel.Start(%s)", fmtifaces(s))
}
// Start specifies the primary value to start from (inclusive).
func Start(id ...interface{}) IteratorOption {
return start(id)
}
type finish []interface{}
func (f finish) apply(i *iterator) {
i.finish = f
}
// String representation.
func (f finish) String() string {
return fmt.Sprintf("rel.Finish(%s)", fmtifaces(f))
}
// Finish specifies the primary value to finish at (inclusive).
func Finish(id ...interface{}) IteratorOption {
return finish(id)
}
type iterator struct {
ctx context.Context
start []interface{}
finish []interface{}
batchSize int
current int
query Query
adapter Adapter
cursor Cursor
fields []string
closed bool
}
func (i *iterator) Close() error {
if !i.closed && i.cursor != nil {
i.closed = true
return i.cursor.Close()
}
return nil
}
func (i *iterator) Next(record interface{}) error {
if i.current%i.batchSize == 0 {
if err := i.fetch(i.ctx, record); err != nil {
return err
}
}
if !i.cursor.Next() {
return io.EOF
}
var (
doc = NewDocument(record)
scanners = doc.Scanners(i.fields)
)
i.current++
return i.cursor.Scan(scanners...)
}
func (i *iterator) fetch(ctx context.Context, record interface{}) error {
if i.current == 0 {
i.init(record)
} else {
i.cursor.Close()
}
i.query = i.query.Limit(i.batchSize).Offset(i.current)
cursor, err := i.adapter.Query(ctx, i.query)
if err != nil {
return err
}
fields, err := cursor.Fields()
if err != nil {
return err
}
i.cursor = cursor
i.fields = fields
return nil
}
func (i *iterator) init(record interface{}) {
var (
doc = NewDocument(record)
)
if i.query.Table == "" {
i.query.Table = doc.Table()
}
if len(i.start) > 0 {
i.query = i.query.Where(filterDocumentPrimary(doc.PrimaryFields(), i.start, FilterGteOp))
}
if len(i.finish) > 0 {
i.query = i.query.Where(filterDocumentPrimary(doc.PrimaryFields(), i.finish, FilterLteOp))
}
i.query = i.query.SortAsc(doc.PrimaryFields()...)
}
func newIterator(ctx context.Context, adapter Adapter, query Query, options []IteratorOption) Iterator {
it := &iterator{
ctx: ctx,
batchSize: 1000,
query: query,
adapter: adapter,
}
for i := range options {
options[i].apply(it)
}
return it
}

@ -0,0 +1,145 @@
package rel
// JoinQuery defines join clause in query.
type JoinQuery struct {
Mode string
Table string
From string
To string
Assoc string
Filter FilterQuery
Arguments []interface{}
}
// Build query.
func (jq JoinQuery) Build(query *Query) {
query.JoinQuery = append(query.JoinQuery, jq)
if jq.Assoc != "" {
query.AddPopulator(&query.JoinQuery[len(query.JoinQuery)-1])
}
}
func (jq *JoinQuery) Populate(query *Query, docMeta DocumentMeta) {
var (
assocMeta = docMeta.Association(jq.Assoc)
assocDocMeta = assocMeta.DocumentMeta()
)
jq.Table = assocDocMeta.Table() + " as " + jq.Assoc
jq.To = jq.Assoc + "." + assocMeta.ForeignField()
jq.From = docMeta.Table() + "." + assocMeta.ReferenceField()
// load association if defined and supported
if assocMeta.Type() == HasOne || assocMeta.Type() == BelongsTo {
var (
load = false
selectField = jq.Assoc + ".*"
)
for i := range query.SelectQuery.Fields {
if load && i > 0 {
query.SelectQuery.Fields[i-1] = query.SelectQuery.Fields[i]
}
if query.SelectQuery.Fields[i] == selectField {
load = true
}
}
if load {
fields := make([]string, len(assocDocMeta.Fields()))
for i, f := range assocDocMeta.Fields() {
fields[i] = jq.Assoc + "." + f + " as " + jq.Assoc + "." + f
}
query.SelectQuery.Fields = append(query.SelectQuery.Fields[:(len(query.SelectQuery.Fields)-1)], fields...)
}
}
}
// NewJoinWith query with custom join mode, table, field and additional filters with AND condition.
func NewJoinWith(mode string, table string, from string, to string, filter ...FilterQuery) JoinQuery {
return JoinQuery{
Mode: mode,
Table: table,
From: from,
To: to,
Filter: And(filter...),
}
}
// NewJoinFragment defines a join clause using raw query.
func NewJoinFragment(expr string, args ...interface{}) JoinQuery {
if args == nil {
// prevent buildJoin to populate From and To variable.
args = []interface{}{}
}
return JoinQuery{
Mode: expr,
Arguments: args,
}
}
// NewJoin with given table.
func NewJoin(table string, filter ...FilterQuery) JoinQuery {
return NewJoinWith("JOIN", table, "", "", filter...)
}
// NewJoinOn table with given field and optional additional filter.
func NewJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
return NewJoinWith("JOIN", table, from, to, filter...)
}
// NewInnerJoin with given table and optional filter.
func NewInnerJoin(table string, filter ...FilterQuery) JoinQuery {
return NewInnerJoinOn(table, "", "", filter...)
}
// NewInnerJoinOn table with given field and optional additional filter.
func NewInnerJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
return NewJoinWith("INNER JOIN", table, from, to, filter...)
}
// NewLeftJoin with given table and optional filter.
func NewLeftJoin(table string, filter ...FilterQuery) JoinQuery {
return NewLeftJoinOn(table, "", "", filter...)
}
// NewLeftJoinOn table with given field and optional additional filter.
func NewLeftJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
return NewJoinWith("LEFT JOIN", table, from, to, filter...)
}
// NewRightJoin with given table and optional filter.
func NewRightJoin(table string, filter ...FilterQuery) JoinQuery {
return NewRightJoinOn(table, "", "", filter...)
}
// NewRightJoinOn table with given field and optional additional filter.
func NewRightJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
return NewJoinWith("RIGHT JOIN", table, from, to, filter...)
}
// NewFullJoin with given table and optional filter.
func NewFullJoin(table string, filter ...FilterQuery) JoinQuery {
return NewFullJoinOn(table, "", "", filter...)
}
// NewFullJoinOn table with given field and optional additional filter.
func NewFullJoinOn(table string, from string, to string, filter ...FilterQuery) JoinQuery {
return NewJoinWith("FULL JOIN", table, from, to, filter...)
}
// NewJoinAssocWith with given association field and optional additional filters.
func NewJoinAssocWith(mode string, assoc string, filter ...FilterQuery) JoinQuery {
return JoinQuery{
Mode: mode,
Assoc: assoc,
Filter: And(filter...),
}
}
// NewJoinAssoc with given association field and optional additional filters.
func NewJoinAssoc(assoc string, filter ...FilterQuery) JoinQuery {
return NewJoinAssocWith("JOIN", assoc, filter...)
}

@ -0,0 +1,66 @@
package rel
// KeyType definition.
type KeyType string
const (
// PrimaryKey KeyType.
PrimaryKey KeyType = "PRIMARY KEY"
// ForeignKey KeyType.
ForeignKey KeyType = "FOREIGN KEY"
// UniqueKey KeyType.
UniqueKey = "UNIQUE"
)
// ForeignKeyReference definition.
type ForeignKeyReference struct {
Table string
Columns []string
OnDelete string
OnUpdate string
}
// Key definition.
type Key struct {
Op SchemaOp
Name string
Type KeyType
Columns []string
Rename string
Reference ForeignKeyReference
Options string
}
func (Key) internalTableDefinition() {}
func createKeys(columns []string, typ KeyType, options []KeyOption) Key {
key := Key{
Op: SchemaCreate,
Columns: columns,
Type: typ,
}
applyKeyOptions(&key, options)
return key
}
func createPrimaryKeys(columns []string, options []KeyOption) Key {
return createKeys(columns, PrimaryKey, options)
}
func createForeignKey(column string, refTable string, refColumn string, options []KeyOption) Key {
key := Key{
Op: SchemaCreate,
Type: ForeignKey,
Columns: []string{column},
Reference: ForeignKeyReference{
Table: refTable,
Columns: []string{refColumn},
},
}
applyKeyOptions(&key, options)
return key
}
// TODO: Rename and Drop, PR welcomed.

162
vendor/github.com/go-rel/rel/map.go generated vendored

@ -0,0 +1,162 @@
package rel
import (
"fmt"
"strings"
)
// Map can be used as mutation for repository insert or update operation.
// This allows inserting or updating only on specified field.
// Insert/Update of has one or belongs to can be done using other Map as a value.
// Insert/Update of has many can be done using slice of Map as a value.
// Map is intended to be used internally within application, and not to be exposed directly as an APIs.
type Map map[string]interface{}
// Apply mutation.
func (m Map) Apply(doc *Document, mutation *Mutation) {
var (
pField = doc.PrimaryField()
pValue = doc.PrimaryValue()
)
for field, value := range m {
switch v := value.(type) {
case Map:
if !mutation.Cascade {
continue
}
var (
assoc = doc.Association(field)
)
if assoc.Type() != HasOne && assoc.Type() != BelongsTo {
panic(fmt.Sprint("rel: cannot associate has many", v, "as", field, "into", doc.Table()))
}
var (
assocDoc, _ = assoc.Document()
assocMutation = Apply(assocDoc, v)
)
mutation.SetAssoc(field, assocMutation)
case []Map:
if !mutation.Cascade {
continue
}
var (
assoc = doc.Association(field)
muts, deletedIDs = applyMaps(v, assoc)
)
mutation.SetAssoc(field, muts...)
mutation.SetDeletedIDs(field, deletedIDs)
default:
if field == pField {
if v != pValue {
panic(fmt.Sprint("rel: replacing primary value (", pValue, " become ", v, ") is not allowed"))
} else {
continue
}
}
if !doc.SetValue(field, v) {
panic(fmt.Sprint("rel: cannot assign ", v, " as ", field, " into ", doc.Table()))
}
mutation.Add(Set(field, v))
}
}
}
func (m Map) String() string {
var builder strings.Builder
builder.WriteString("rel.Map{")
for k, v := range m {
if builder.Len() > len("rel.Map{") {
builder.WriteString(", ")
}
builder.WriteByte('"')
builder.WriteString(k)
builder.WriteString("\": ")
switch im := v.(type) {
case Map:
builder.WriteString(im.String())
case []Map:
builder.WriteString("[]rel.Map{")
for i := range im {
if i > 0 {
builder.WriteString(", ")
}
builder.WriteString(im[i].String())
}
builder.WriteString("}")
default:
builder.WriteString(fmtiface(v)) // TODO: use compact struct print (reltest.csprint)
}
}
builder.WriteString("}")
return builder.String()
}
func applyMaps(maps []Map, assoc Association) ([]Mutation, []interface{}) {
var (
deletedIDs []interface{}
muts = make([]Mutation, len(maps))
col, _ = assoc.Collection()
)
var (
pField = col.PrimaryField()
pIndex = make(map[interface{}]int)
pValues = col.PrimaryValue().([]interface{})
)
for i, v := range pValues {
pIndex[v] = i
}
var (
curr = 0
inserts []Map
)
for _, m := range maps {
if pChange, changed := m[pField]; changed {
// update
pID, ok := pIndex[pChange]
if !ok {
panic("rel: cannot update has many assoc that is not loaded or doesn't belong to this record")
}
if pID != curr {
col.Swap(pID, curr)
pValues[pID], pValues[curr] = pValues[curr], pValues[pID]
}
muts[curr] = Apply(col.Get(curr), m)
delete(pIndex, pChange)
curr++
} else {
inserts = append(inserts, m)
}
}
// delete stales
if curr < col.Len() {
deletedIDs = pValues[curr:]
col.Truncate(0, curr)
} else {
deletedIDs = []interface{}{}
}
// inserts remaining
for i, m := range inserts {
muts[curr+i] = Apply(col.Add(), m)
}
return muts, deletedIDs
}

@ -0,0 +1,278 @@
package rel
import (
"fmt"
"reflect"
)
// Mutator is interface for a record mutator.
type Mutator interface {
Apply(doc *Document, mutation *Mutation)
}
// Apply using given mutators.
func Apply(doc *Document, mutators ...Mutator) Mutation {
return applyMutators(doc, true, true, mutators...)
}
// apply given mutators with customized default values
func applyMutators(doc *Document, cascade, applyStructset bool, mutators ...Mutator) Mutation {
var (
optionsCount int
mutation = Mutation{
Unscoped: false,
Reload: false,
Cascade: Cascade(cascade),
}
)
for i := range mutators {
switch mut := mutators[i].(type) {
case Unscoped, Reload, Cascade, OnConflict:
optionsCount++
mut.Apply(doc, &mutation)
default:
mut.Apply(doc, &mutation)
}
}
// fallback to structset.
if applyStructset && optionsCount == len(mutators) {
newStructset(doc, false).Apply(doc, &mutation)
}
return mutation
}
// AssocMutation represents mutation for association.
type AssocMutation struct {
Mutations []Mutation
DeletedIDs []interface{} // This is array of single id, and doesn't support composite primary key.
}
// Mutation represents value to be inserted or updated to database.
// It's not safe to be used multiple time. some operation my alter mutation data.
type Mutation struct {
Mutates map[string]Mutate
Assoc map[string]AssocMutation
OnConflict OnConflict
Unscoped Unscoped
Reload Reload
Cascade Cascade
ErrorFunc ErrorFunc
}
func (m *Mutation) initMutates() {
if m.Mutates == nil {
m.Mutates = make(map[string]Mutate)
}
}
func (m *Mutation) initAssoc() {
if m.Assoc == nil {
m.Assoc = make(map[string]AssocMutation)
}
}
// IsEmpty returns true if no mutates operation and assoc's mutation is defined.
func (m *Mutation) IsEmpty() bool {
return m.IsMutatesEmpty() && m.IsAssocEmpty()
}
// IsMutatesEmpty returns true if no mutates operation is defined.
func (m *Mutation) IsMutatesEmpty() bool {
return len(m.Mutates) == 0
}
// IsAssocEmpty returns true if no assoc's mutation is defined.
func (m *Mutation) IsAssocEmpty() bool {
return len(m.Assoc) == 0
}
// Add a mutate.
func (m *Mutation) Add(mut Mutate) {
m.initMutates()
m.Mutates[mut.Field] = mut
}
// SetAssoc mutation.
func (m *Mutation) SetAssoc(field string, muts ...Mutation) {
m.initAssoc()
assoc := m.Assoc[field]
assoc.Mutations = muts
m.Assoc[field] = assoc
}
// SetDeletedIDs mutation.
// nil slice will clear association.
func (m *Mutation) SetDeletedIDs(field string, ids []interface{}) {
m.initAssoc()
assoc := m.Assoc[field]
assoc.DeletedIDs = ids
m.Assoc[field] = assoc
}
// ChangeOp represents type of mutate operation.
type ChangeOp int
const (
// ChangeInvalidOp operation.
ChangeInvalidOp ChangeOp = iota
// ChangeSetOp operation.
ChangeSetOp
// ChangeIncOp operation.
ChangeIncOp
// ChangeFragmentOp operation.
ChangeFragmentOp
)
// Mutate stores mutation instruction.
type Mutate struct {
Type ChangeOp
Field string
Value interface{}
}
// Apply mutation.
func (m Mutate) Apply(doc *Document, mutation *Mutation) {
invalid := false
switch m.Type {
case ChangeSetOp:
if !doc.SetValue(m.Field, m.Value) {
invalid = true
}
case ChangeFragmentOp:
mutation.Reload = true
default:
if typ, ok := doc.Type(m.Field); ok {
kind := typ.Kind()
invalid = m.Type == ChangeIncOp && (kind < reflect.Int || kind > reflect.Uint64)
} else {
invalid = true
}
mutation.Reload = true
}
if invalid {
panic(fmt.Sprint("rel: cannot assign ", m.Value, " as ", m.Field, " into ", doc.Table()))
}
mutation.Add(m)
}
// String representation
func (m Mutate) String() string {
str := "≤Invalid Mutator>"
switch m.Type {
case ChangeSetOp:
str = fmt.Sprintf("rel.Set(\"%s\", %s)", m.Field, fmtiface(m.Value))
case ChangeIncOp:
str = fmt.Sprintf("rel.IncBy(\"%s\", %s)", m.Field, fmtiface(m.Value))
case ChangeFragmentOp:
str = fmt.Sprintf("rel.SetFragment(\"%s\", %s)", m.Field, fmtifaces(m.Value.([]interface{})))
}
return str
}
// Set create a mutate using set operation.
func Set(field string, value interface{}) Mutate {
return Mutate{
Type: ChangeSetOp,
Field: field,
Value: value,
}
}
// Inc create a mutate using increment operation.
func Inc(field string) Mutate {
return IncBy(field, 1)
}
// IncBy create a mutate using increment operation with custom increment value.
func IncBy(field string, n int) Mutate {
return Mutate{
Type: ChangeIncOp,
Field: field,
Value: n,
}
}
// Dec create a mutate using deccrement operation.
func Dec(field string) Mutate {
return DecBy(field, 1)
}
// DecBy create a mutate using decrement operation with custom decrement value.
func DecBy(field string, n int) Mutate {
return Mutate{
Type: ChangeIncOp,
Field: field,
Value: -n,
}
}
// SetFragment create a mutate operation using fragment operation.
// Only available for Update.
func SetFragment(raw string, args ...interface{}) Mutate {
return Mutate{
Type: ChangeFragmentOp,
Field: raw,
Value: args,
}
}
// Setf is an alias for SetFragment
var Setf = SetFragment
// Reload force reload after insert/update.
// Default to false.
type Reload bool
// Apply mutation.
func (r Reload) Apply(doc *Document, mutation *Mutation) {
mutation.Reload = r
}
// Build query.
func (r Reload) Build(query *Query) {
query.ReloadQuery = r
}
// Cascade enable or disable updating associations.
// Default to true.
type Cascade bool
// Build query.
func (c Cascade) Build(query *Query) {
query.CascadeQuery = c
}
// Apply mutation.
func (c Cascade) Apply(doc *Document, mutation *Mutation) {
mutation.Cascade = c
}
func (c Cascade) String() string {
return fmt.Sprintf("rel.Cascade(%t)", c)
}
// ErrorFunc allows conversion REL's error to Application custom errors.
type ErrorFunc func(error) error
// Apply mutation.
func (ef ErrorFunc) Apply(doc *Document, mutation *Mutation) {
mutation.ErrorFunc = ef
}
func (ef ErrorFunc) transform(err error) error {
if ef != nil && err != nil {
return ef(err)
}
return err
}

@ -0,0 +1,37 @@
package rel
import (
"database/sql"
"reflect"
)
type nullable struct {
dest interface{}
}
var _ sql.Scanner = (*nullable)(nil)
func (n nullable) Scan(src interface{}) error {
return convertAssign(n.dest, src)
}
// Nullable wrap value as a nullable sql.Scanner.
// If value returned from database is nil, nullable scanner will set dest to zero value.
func Nullable(dest interface{}) interface{} {
if s, ok := dest.(sql.Scanner); ok {
return s
}
rt := reflect.TypeOf(dest)
if rt.Kind() != reflect.Ptr {
panic("rel: destination must be a pointer")
}
if rt.Elem().Kind() == reflect.Ptr {
return dest
}
return nullable{
dest: dest,
}
}

@ -0,0 +1,64 @@
package rel
// OnConflict mutation.
type OnConflict struct {
Keys []string
Ignore bool
Replace bool
Fragment string
FragmentArgs []interface{}
}
// Apply mutation.
func (ocm OnConflict) Apply(doc *Document, mutation *Mutation) {
if ocm.Keys == nil && ocm.Fragment == "" {
ocm.Keys = doc.PrimaryFields()
}
mutation.OnConflict = ocm
}
// OnConflictIgnore insertion when conflict happens.
func OnConflictIgnore() OnConflict {
return OnConflict{Ignore: true}
}
// OnConflictKeyIgnore insertion when conflict happens on specific keys.
//
// Specifying key is not supported by all database and may be ignored.
func OnConflictKeyIgnore(key string) OnConflict {
return OnConflictKeysIgnore([]string{key})
}
// OnConflictKeysIgnore insertion when conflict happens on specific keys.
//
// Specifying key is not supported by all database and may be ignored.
func OnConflictKeysIgnore(keys []string) OnConflict {
return OnConflict{Keys: keys, Ignore: true}
}
// OnConflictReplace insertion when conflict happens.
func OnConflictReplace() OnConflict {
return OnConflict{Replace: true}
}
// OnConflictKeyReplace insertion when conflict happens on specific keys.
//
// Specifying key is not supported by all database and may be ignored.
func OnConflictKeyReplace(key string) OnConflict {
return OnConflictKeysReplace([]string{key})
}
// OnConflictKeysReplace insertion when conflict happens on specific keys.
//
// Specifying key is not supported by all database and may be ignored.
func OnConflictKeysReplace(keys []string) OnConflict {
return OnConflict{Keys: keys, Replace: true}
}
// OnConflictFragment allows to write custom sql for on conflict.
//
// This will add custom sql after ON CONFLICT, example: ON CONFLICT [FRAGMENT]
func OnConflictFragment(sql string, args ...interface{}) OnConflict {
return OnConflict{Fragment: sql, FragmentArgs: args}
}

@ -0,0 +1,574 @@
package rel
import (
"strconv"
"strings"
)
// Querier interface defines contract to be used for query builder.
type Querier interface {
Build(*Query)
}
type QueryPopulator interface {
Populate(*Query, DocumentMeta)
}
// Build for given table using given queriers.
func Build(table string, queriers ...Querier) Query {
var (
query = newQuery()
)
if len(queriers) > 0 {
_, query.empty = queriers[0].(Query)
}
for _, querier := range queriers {
// avoid using indirect call to avoid heap allocation
switch q := querier.(type) {
case Query:
q.Build(&query)
case JoinQuery:
q.Build(&query)
case FilterQuery:
q.Build(&query)
case GroupQuery:
q.Build(&query)
case SortQuery:
q.Build(&query)
case Offset:
q.Build(&query)
case Limit:
q.Build(&query)
case Lock:
q.Build(&query)
case Unscoped:
q.Build(&query)
case Reload:
q.Build(&query)
case SQLQuery:
q.Build(&query)
case Preload:
q.Build(&query)
case Cascade:
q.Build(&query)
}
}
if query.Table == "" {
query.Table = table
}
return query
}
// Query defines information about query generated by query builder.
type Query struct {
empty bool // TODO: use bitmask to mark what is updated and use it when merging two queries
Table string
SelectQuery SelectQuery
JoinQuery []JoinQuery
WhereQuery FilterQuery
GroupQuery GroupQuery
SortQuery []SortQuery
OffsetQuery Offset
LimitQuery Limit
LockQuery Lock
SQLQuery SQLQuery
UnscopedQuery Unscoped
ReloadQuery Reload
CascadeQuery Cascade
PreloadQuery []string
UsePrimaryDb bool
queryPopulators []QueryPopulator
}
// Build query.
func (q Query) Build(query *Query) {
if query.empty {
*query = q
} else {
// manual merge
if q.Table != "" {
query.Table = q.Table
}
if q.SelectQuery.Fields != nil {
query.SelectQuery = q.SelectQuery
}
query.JoinQuery = append(query.JoinQuery, q.JoinQuery...)
if !q.WhereQuery.None() {
query.WhereQuery = query.WhereQuery.And(q.WhereQuery)
}
if q.GroupQuery.Fields != nil {
query.GroupQuery = q.GroupQuery
}
query.SortQuery = append(query.SortQuery, q.SortQuery...)
if q.OffsetQuery != 0 {
query.OffsetQuery = q.OffsetQuery
}
if q.LimitQuery != 0 {
query.LimitQuery = q.LimitQuery
}
if q.LockQuery != "" {
query.LockQuery = q.LockQuery
}
query.ReloadQuery = query.ReloadQuery || q.ReloadQuery
query.CascadeQuery = query.CascadeQuery || q.CascadeQuery
query.UsePrimaryDb = query.UsePrimaryDb || q.UsePrimaryDb
}
}
func (q Query) Populate(documentMeta DocumentMeta) Query {
for i := range q.queryPopulators {
q.queryPopulators[i].Populate(&q, documentMeta)
}
return q
}
func (q *Query) AddPopulator(populator QueryPopulator) {
q.queryPopulators = append(q.queryPopulators, populator)
}
// Select filter fields to be selected from database.
func (q Query) Select(fields ...string) Query {
q.SelectQuery = NewSelect(fields...)
return q
}
// From set the table to be used for query.
func (q Query) From(table string) Query {
q.Table = table
return q
}
// Distinct sets select query to be distinct.
func (q Query) Distinct() Query {
q.SelectQuery.OnlyDistinct = true
return q
}
// Join current table with other table.
func (q Query) Join(table string, filter ...FilterQuery) Query {
return q.JoinOn(table, "", "", filter...)
}
// JoinOn current table with other table.
func (q Query) JoinOn(table string, from string, to string, filter ...FilterQuery) Query {
return q.JoinWith("JOIN", table, from, to, filter...)
}
// JoinWith current table with other table with custom join mode.
func (q Query) JoinWith(mode string, table string, from string, to string, filter ...FilterQuery) Query {
NewJoinWith(mode, table, from, to, filter...).Build(&q) // TODO: ensure this always called last
return q
}
// Joinf create join query using a raw query.
func (q Query) Joinf(expr string, args ...interface{}) Query {
NewJoinFragment(expr, args...).Build(&q) // TODO: ensure this always called last
return q
}
// JoinAssoc current table with other table based on association field.
func (q Query) JoinAssoc(assoc string, filter ...FilterQuery) Query {
return q.JoinAssocWith("JOIN", assoc, filter...)
}
// JoinAssocWith current table with other table based on association field.
func (q Query) JoinAssocWith(mode string, assoc string, filter ...FilterQuery) Query {
NewJoinAssocWith(mode, assoc, filter...).Build(&q)
return q
}
// Where query.
func (q Query) Where(filters ...FilterQuery) Query {
q.WhereQuery = q.WhereQuery.And(filters...)
return q
}
// Wheref create where query using a raw query.
func (q Query) Wheref(expr string, args ...interface{}) Query {
q.WhereQuery = q.WhereQuery.And(FilterFragment(expr, args...))
return q
}
// OrWhere query.
func (q Query) OrWhere(filters ...FilterQuery) Query {
q.WhereQuery = q.WhereQuery.Or(And(filters...))
return q
}
// OrWheref create where query using a raw query.
func (q Query) OrWheref(expr string, args ...interface{}) Query {
q.WhereQuery = q.WhereQuery.Or(FilterFragment(expr, args...))
return q
}
// Group query.
func (q Query) Group(fields ...string) Query {
q.GroupQuery.Fields = fields
return q
}
// Having query.
func (q Query) Having(filters ...FilterQuery) Query {
q.GroupQuery.Filter = q.GroupQuery.Filter.And(filters...)
return q
}
// Havingf create having query using a raw query.
func (q Query) Havingf(expr string, args ...interface{}) Query {
q.GroupQuery.Filter = q.GroupQuery.Filter.And(FilterFragment(expr, args...))
return q
}
// OrHaving query.
func (q Query) OrHaving(filters ...FilterQuery) Query {
q.GroupQuery.Filter = q.GroupQuery.Filter.Or(And(filters...))
return q
}
// OrHavingf create having query using a raw query.
func (q Query) OrHavingf(expr string, args ...interface{}) Query {
q.GroupQuery.Filter = q.GroupQuery.Filter.Or(FilterFragment(expr, args...))
return q
}
// Sort query.
func (q Query) Sort(fields ...string) Query {
return q.SortAsc(fields...)
}
// SortAsc query.
func (q Query) SortAsc(fields ...string) Query {
var (
offset = len(q.SortQuery)
)
q.SortQuery = append(q.SortQuery, make([]SortQuery, len(fields))...)
for i := range fields {
q.SortQuery[offset+i] = NewSortAsc(fields[i])
}
return q
}
// SortDesc query.
func (q Query) SortDesc(fields ...string) Query {
var (
offset = len(q.SortQuery)
)
q.SortQuery = append(q.SortQuery, make([]SortQuery, len(fields))...)
for i := range fields {
q.SortQuery[offset+i] = NewSortDesc(fields[i])
}
return q
}
// Offset the result returned by database.
func (q Query) Offset(offset int) Query {
q.OffsetQuery = Offset(offset)
return q
}
// Limit result returned by database.
func (q Query) Limit(limit int) Query {
q.LimitQuery = Limit(limit)
return q
}
// Lock query expression.
func (q Query) Lock(lock string) Query {
q.LockQuery = Lock(lock)
return q
}
// Unscoped allows soft-delete to be ignored.
func (q Query) Unscoped() Query {
q.UnscopedQuery = true
return q
}
// Reload force reloading association on preload.
func (q Query) Reload() Query {
q.ReloadQuery = true
return q
}
// Cascade enable/disable autoload association on Find and FindAll query.
func (q Query) Cascade(c bool) Query {
q.CascadeQuery = Cascade(c)
return q
}
// Preload field association.
func (q Query) Preload(field string) Query {
q.PreloadQuery = append(q.PreloadQuery, field)
return q
}
// UsePrimary database.
func (q Query) UsePrimary() Query {
q.UsePrimaryDb = true
return q
}
// String describe query as string.
func (q Query) String() string {
if q.SQLQuery.Statement != "" {
return q.SQLQuery.String()
}
var builder strings.Builder
builder.WriteString("rel")
if q.UsePrimaryDb {
builder.WriteString(".UsePrimary()")
}
if q.Table != "" {
builder.WriteString(".From(\"")
builder.WriteString(q.Table)
builder.WriteString("\")")
}
if len(q.SelectQuery.Fields) != 0 {
builder.WriteString(".Select(\"")
builder.WriteString(strings.Join(q.SelectQuery.Fields, "\", \""))
builder.WriteString("\")")
}
if q.SelectQuery.OnlyDistinct {
builder.WriteString(".Distinct()")
}
for i := range q.JoinQuery {
builder.WriteString(".JoinWith(\"")
builder.WriteString(q.JoinQuery[i].Mode)
builder.WriteString("\", \"")
builder.WriteString(q.JoinQuery[i].Table)
builder.WriteString("\", \"")
builder.WriteString(q.JoinQuery[i].From)
builder.WriteString("\", \"")
builder.WriteString(q.JoinQuery[i].To)
builder.WriteString("\")")
}
if !q.WhereQuery.None() {
builder.WriteString(".Where(")
builder.WriteString(q.WhereQuery.String())
builder.WriteByte(')')
}
if len(q.GroupQuery.Fields) != 0 {
builder.WriteString(".Group(\"")
builder.WriteString(strings.Join(q.GroupQuery.Fields, "\", \""))
builder.WriteString("\")")
if !q.GroupQuery.Filter.None() {
builder.WriteString(".Having(")
builder.WriteString(q.GroupQuery.Filter.String())
builder.WriteByte(')')
}
}
for _, sq := range q.SortQuery {
if sq.Asc() {
builder.WriteString(".SortAsc(\"")
} else {
builder.WriteString(".SortDesc(\"")
}
builder.WriteString(sq.Field)
builder.WriteString("\")")
}
if q.LimitQuery > 0 {
builder.WriteString(".Limit(")
builder.WriteString(strconv.Itoa(int(q.LimitQuery)))
builder.WriteString(")")
}
if q.OffsetQuery > 0 {
builder.WriteString(".Offset(")
builder.WriteString(strconv.Itoa(int(q.OffsetQuery)))
builder.WriteString(")")
}
if q.LockQuery != "" {
builder.WriteString(".Lock(\"")
builder.WriteString(string(q.LockQuery))
builder.WriteString("\")")
}
if q.UnscopedQuery {
builder.WriteString(".Unscoped()")
}
if q.ReloadQuery {
builder.WriteString(".Reload()")
}
if !q.CascadeQuery {
builder.WriteString(".Cascade(false)")
}
if len(q.PreloadQuery) != 0 {
builder.WriteString(".Preload(\"")
builder.WriteString(strings.Join(q.PreloadQuery, "\", \""))
builder.WriteString("\")")
}
if str := builder.String(); str != "rel" {
return str
}
return ""
}
func newQuery() Query {
return Query{
CascadeQuery: true,
}
}
// Select query create a query with chainable syntax, using select as the starting point.
func Select(fields ...string) Query {
query := newQuery()
query.SelectQuery.Fields = fields
return query
}
// From create a query with chainable syntax, using from as the starting point.
func From(table string) Query {
query := newQuery()
query.Table = table
return query
}
// Join create a query with chainable syntax, using join as the starting point.
func Join(table string, filter ...FilterQuery) Query {
return JoinOn(table, "", "", filter...)
}
// JoinOn create a query with chainable syntax, using join as the starting point.
func JoinOn(table string, from string, to string, filter ...FilterQuery) Query {
return JoinWith("JOIN", table, from, to, filter...)
}
// JoinWith create a query with chainable syntax, using join as the starting point.
func JoinWith(mode string, table string, from string, to string, filter ...FilterQuery) Query {
query := newQuery()
query.JoinQuery = []JoinQuery{
NewJoinWith(mode, table, from, to, filter...),
}
return query
}
// JoinAssoc create a query with chainable syntax, using join as the starting point.
func JoinAssoc(assoc string, filter ...FilterQuery) Query {
return JoinAssocWith("JOIN", assoc, filter...)
}
// JoinAssocWith create a query with chainable syntax, using join as the starting point.
func JoinAssocWith(mode string, assoc string, filter ...FilterQuery) Query {
query := newQuery()
query.JoinQuery = []JoinQuery{
NewJoinAssocWith(mode, assoc, filter...),
}
query.AddPopulator(&query.JoinQuery[0])
return query
}
// Joinf create a query with chainable syntax, using join as the starting point.
func Joinf(expr string, args ...interface{}) Query {
query := newQuery()
query.JoinQuery = []JoinQuery{
NewJoinFragment(expr, args...),
}
return query
}
// Where create a query with chainable syntax, using where as the starting point.
func Where(filters ...FilterQuery) Query {
query := newQuery()
query.WhereQuery = And(filters...)
return query
}
func UsePrimary() Query {
query := newQuery()
query.UsePrimaryDb = true
return query
}
// Offset Query.
type Offset int
// Build query.
func (o Offset) Build(query *Query) {
query.OffsetQuery = o
}
// Limit options.
// When passed as query, it limits returned result from database.
// When passed as column option, it sets the maximum size of the string/text/binary/integer columns.
type Limit int
// Build query.
func (l Limit) Build(query *Query) {
query.LimitQuery = l
}
func (l Limit) applyColumn(column *Column) {
column.Limit = int(l)
}
// Lock query.
// This query will be ignored if used outside of transaction.
type Lock string
// Build query.
func (l Lock) Build(query *Query) {
query.LockQuery = l
}
// ForUpdate lock query.
func ForUpdate() Lock {
return "FOR UPDATE"
}
// Unscoped query.
type Unscoped bool
// Build query.
func (u Unscoped) Build(query *Query) {
query.UnscopedQuery = u
}
// Apply mutation.
func (u Unscoped) Apply(doc *Document, mutation *Mutation) {
mutation.Unscoped = u
}
// Preload query.
type Preload string
// Build query.
func (p Preload) Build(query *Query) {
query.PreloadQuery = append(query.PreloadQuery, string(p))
}

@ -0,0 +1,2 @@
// Package rel contains all rel primary APIs, such as Repository.
package rel

File diff suppressed because it is too large Load Diff

@ -0,0 +1,150 @@
package rel
import (
"context"
"strings"
)
// SchemaOp type.
type SchemaOp uint8
const (
// SchemaCreate operation.
SchemaCreate SchemaOp = iota
// SchemaAlter operation.
SchemaAlter
// SchemaRename operation.
SchemaRename
// SchemaDrop operation.
SchemaDrop
)
func (s SchemaOp) String() string {
return [...]string{"create", "alter", "rename", "drop"}[s]
}
// Migration definition.
type Migration interface {
internalMigration()
description() string
}
// Schema builder.
type Schema struct {
Migrations []Migration
}
func (s *Schema) add(migration Migration) {
s.Migrations = append(s.Migrations, migration)
}
// CreateTable with name and its definition.
func (s *Schema) CreateTable(name string, fn func(t *Table), options ...TableOption) {
table := createTable(name, options)
fn(&table)
s.add(table)
}
// CreateTableIfNotExists with name and its definition.
func (s *Schema) CreateTableIfNotExists(name string, fn func(t *Table), options ...TableOption) {
table := createTableIfNotExists(name, options)
fn(&table)
s.add(table)
}
// AlterTable with name and its definition.
func (s *Schema) AlterTable(name string, fn func(t *AlterTable), options ...TableOption) {
table := alterTable(name, options)
fn(&table)
s.add(table.Table)
}
// RenameTable by name.
func (s *Schema) RenameTable(name string, newName string, options ...TableOption) {
s.add(renameTable(name, newName, options))
}
// DropTable by name.
func (s *Schema) DropTable(name string, options ...TableOption) {
s.add(dropTable(name, options))
}
// DropTableIfExists by name.
func (s *Schema) DropTableIfExists(name string, options ...TableOption) {
s.add(dropTableIfExists(name, options))
}
// AddColumn with name and type.
func (s *Schema) AddColumn(table string, name string, typ ColumnType, options ...ColumnOption) {
at := alterTable(table, nil)
at.Column(name, typ, options...)
s.add(at.Table)
}
// RenameColumn by name.
func (s *Schema) RenameColumn(table string, name string, newName string, options ...ColumnOption) {
at := alterTable(table, nil)
at.RenameColumn(name, newName, options...)
s.add(at.Table)
}
// DropColumn by name.
func (s *Schema) DropColumn(table string, name string, options ...ColumnOption) {
at := alterTable(table, nil)
at.DropColumn(name, options...)
s.add(at.Table)
}
// CreateIndex for columns on a table.
func (s *Schema) CreateIndex(table string, name string, column []string, options ...IndexOption) {
s.add(createIndex(table, name, column, options))
}
// CreateUniqueIndex for columns on a table.
func (s *Schema) CreateUniqueIndex(table string, name string, column []string, options ...IndexOption) {
s.add(createUniqueIndex(table, name, column, options))
}
// DropIndex by name.
func (s *Schema) DropIndex(table string, name string, options ...IndexOption) {
s.add(dropIndex(table, name, options))
}
// Exec queries.
func (s *Schema) Exec(raw Raw) {
s.add(raw)
}
// Do migration using golang codes.
func (s *Schema) Do(fn Do) {
s.add(fn)
}
// String returns schema operation.
func (s Schema) String() string {
descs := make([]string, len(s.Migrations))
for i := range descs {
descs[i] = s.Migrations[i].description()
}
return strings.Join(descs, ", ")
}
// Raw string
type Raw string
func (r Raw) description() string {
return "execute raw command"
}
func (r Raw) internalMigration() {}
func (r Raw) internalTableDefinition() {}
// Do used internally for schema migration.
type Do func(context.Context, Repository) error
func (d Do) description() string {
return "run go code"
}
func (d Do) internalMigration() {}

@ -0,0 +1,142 @@
package rel
// TableOption interface.
// Available options are: Comment, Options.
type TableOption interface {
applyTable(table *Table)
}
func applyTableOptions(table *Table, options []TableOption) {
for i := range options {
options[i].applyTable(table)
}
}
// ColumnOption interface.
// Available options are: Nil, Unsigned, Limit, Precision, Scale, Default, Comment, Options.
type ColumnOption interface {
applyColumn(column *Column)
}
func applyColumnOptions(column *Column, options []ColumnOption) {
for i := range options {
options[i].applyColumn(column)
}
}
// KeyOption interface.
// Available options are: Comment, Options.
type KeyOption interface {
applyKey(key *Key)
}
func applyKeyOptions(key *Key, options []KeyOption) {
for i := range options {
options[i].applyKey(key)
}
}
// Primary set column as primary.
type Primary bool
func (r Primary) applyColumn(column *Column) {
column.Primary = bool(r)
}
// Unique set column as unique.
type Unique bool
func (r Unique) applyColumn(column *Column) {
column.Unique = bool(r)
}
func (r Unique) applyIndex(index *Index) {
index.Unique = bool(r)
}
// Required disallows nil values in the column.
type Required bool
func (r Required) applyColumn(column *Column) {
column.Required = bool(r)
}
// Unsigned sets integer column to be unsigned.
type Unsigned bool
func (u Unsigned) applyColumn(column *Column) {
column.Unsigned = bool(u)
}
// Precision defines the precision for the decimal fields, representing the total number of digits in the number.
type Precision int
func (p Precision) applyColumn(column *Column) {
column.Precision = int(p)
}
// Scale Defines the scale for the decimal fields, representing the number of digits after the decimal point.
type Scale int
func (s Scale) applyColumn(column *Column) {
column.Scale = int(s)
}
type defaultValue struct {
value interface{}
}
func (d defaultValue) applyColumn(column *Column) {
column.Default = d.value
}
// Default allows to set a default value on the column.).
func Default(def interface{}) ColumnOption {
return defaultValue{value: def}
}
// OnDelete option for foreign key.
type OnDelete string
func (od OnDelete) applyKey(key *Key) {
key.Reference.OnDelete = string(od)
}
// OnUpdate option for foreign key.
type OnUpdate string
func (ou OnUpdate) applyKey(key *Key) {
key.Reference.OnUpdate = string(ou)
}
// Options options for table, column and index.
type Options string
func (o Options) applyTable(table *Table) {
table.Options = string(o)
}
func (o Options) applyColumn(column *Column) {
column.Options = string(o)
}
func (o Options) applyIndex(index *Index) {
index.Options = string(o)
}
func (o Options) applyKey(key *Key) {
key.Options = string(o)
}
// Optional option.
// when used with create table, will create table only if it's not exists.
// when used with drop table, will drop table only if it's exists.
type Optional bool
func (o Optional) applyTable(table *Table) {
table.Optional = bool(o)
}
func (o Optional) applyIndex(index *Index) {
index.Optional = bool(o)
}

@ -0,0 +1,22 @@
package rel
// SelectQuery defines select clause of the query.
type SelectQuery struct {
OnlyDistinct bool
Fields []string
}
// Distinct select query.
func (sq SelectQuery) Distinct() SelectQuery {
sq.OnlyDistinct = true
return sq
}
// NewSelect query.
//
// Deprecated: use Select instead
func NewSelect(fields ...string) SelectQuery {
return SelectQuery{
Fields: fields,
}
}

@ -0,0 +1,50 @@
package rel
// SortQuery defines sort information of query.
type SortQuery struct {
Field string
Sort int
}
// Build sort query.
func (sq SortQuery) Build(query *Query) {
query.SortQuery = append(query.SortQuery, sq)
}
// Asc returns true if sort is ascending.
func (sq SortQuery) Asc() bool {
return sq.Sort >= 0
}
// Desc returns true if s is descending.
func (sq SortQuery) Desc() bool {
return sq.Sort < 0
}
// SortAsc sorts field with ascending sort.
func SortAsc(field string) SortQuery {
return SortQuery{
Field: field,
Sort: 1,
}
}
// NewSortDesc sorts field with descending sort.
func SortDesc(field string) SortQuery {
return SortQuery{
Field: field,
Sort: -1,
}
}
var (
// NewSortAsc sorts field with ascending sort.
//
// Deprecated: use SortAsc instead
NewSortAsc = SortAsc
// NewSortDesc sorts field with descending sort.
//
// Deprecated: use SortDesc instead
NewSortDesc = SortDesc
)

@ -0,0 +1,37 @@
package rel
import "strings"
// SQLQuery allows querying using native query supported by database.
type SQLQuery struct {
Statement string
Values []interface{}
}
// Build Raw Query.
func (sq SQLQuery) Build(query *Query) {
query.SQLQuery = sq
}
func (sq SQLQuery) String() string {
var builder strings.Builder
builder.WriteString("rel.SQL(\"")
builder.WriteString(sq.Statement)
builder.WriteString("\"")
if len(sq.Values) != 0 {
builder.WriteString(", ")
builder.WriteString(fmtifaces(sq.Values))
}
builder.WriteString(")")
return builder.String()
}
// SQL Query.
func SQL(statement string, values ...interface{}) SQLQuery {
return SQLQuery{
Statement: statement,
Values: values,
}
}

@ -0,0 +1,138 @@
package rel
import (
"fmt"
"time"
)
var (
Now NowFunc = func() time.Time {
return time.Now().Truncate(time.Second)
}
)
// NowFunc is the type of function that returns the current time.
type NowFunc func() time.Time
// Structset can be used as mutation for repository insert or update operation.
// This will save every field in struct and it's association as long as it's loaded.
// This is the default mutator used by repository.
type Structset struct {
doc *Document
skipZero bool
}
// Apply mutation.
func (s Structset) Apply(doc *Document, mut *Mutation) {
var (
pFields = s.doc.PrimaryFields()
t = Now()
)
for _, field := range s.doc.Fields() {
switch field {
case "created_at", "inserted_at":
if doc.Flag(HasCreatedAt) {
if value, ok := doc.Value(field); ok && value.(time.Time).IsZero() {
s.set(doc, mut, field, t, true)
continue
}
}
case "updated_at":
if doc.Flag(HasUpdatedAt) {
s.set(doc, mut, field, t, true)
continue
}
}
if len(pFields) == 1 && pFields[0] == field {
// allow setting primary key as long as it's not zero.
s.applyValue(doc, mut, field, true)
} else {
s.applyValue(doc, mut, field, s.skipZero)
}
}
if mut.Cascade {
s.applyAssoc(mut)
}
}
func (s Structset) set(doc *Document, mut *Mutation, field string, value interface{}, force bool) {
if (force || doc.v != s.doc.v) && !doc.SetValue(field, value) {
panic(fmt.Sprint("rel: cannot assign ", value, " as ", field, " into ", doc.Table()))
}
mut.Add(Set(field, value))
}
func (s Structset) applyValue(doc *Document, mut *Mutation, field string, skipZero bool) {
if value, ok := s.doc.Value(field); ok {
if skipZero && isZero(value) {
return
}
s.set(doc, mut, field, value, false)
}
}
func (s Structset) applyAssoc(mut *Mutation) {
for _, field := range s.doc.BelongsTo() {
s.buildAssoc(field, mut)
}
for _, field := range s.doc.HasOne() {
s.buildAssoc(field, mut)
}
for _, field := range s.doc.HasMany() {
s.buildAssocMany(field, mut)
}
}
func (s Structset) buildAssoc(field string, mut *Mutation) {
assoc := s.doc.Association(field)
if assoc.IsZero() {
return
}
var (
doc, _ = assoc.Document()
)
mut.SetAssoc(field, Apply(doc, newStructset(doc, s.skipZero)))
}
func (s Structset) buildAssocMany(field string, mut *Mutation) {
assoc := s.doc.Association(field)
if assoc.IsZero() {
return
}
var (
col, _ = assoc.Collection()
muts = make([]Mutation, col.Len())
)
for i := range muts {
var (
doc = col.Get(i)
)
muts[i] = Apply(doc, newStructset(doc, s.skipZero))
}
mut.SetAssoc(field, muts...)
}
func newStructset(doc *Document, skipZero bool) Structset {
return Structset{
doc: doc,
skipZero: skipZero,
}
}
// NewStructset from a struct.
func NewStructset(record interface{}, skipZero bool) Structset {
return newStructset(NewDocument(record), skipZero)
}

@ -0,0 +1,27 @@
package rel
// SubQuery warps a query into: Prefix (Query)
type SubQuery struct {
Prefix string
Query Query
}
// All warp a query into ALL(sub-query)
//
// Some database may not support this keyword, please consult to your database documentation.
func All(sub Query) SubQuery {
return SubQuery{
Prefix: "ALL",
Query: sub,
}
}
// Any warp a query into ANY(sub-query)
//
// Some database may not support this keyword, please consult to your database documentation.
func Any(sub Query) SubQuery {
return SubQuery{
Prefix: "ANY",
Query: sub,
}
}

@ -0,0 +1,195 @@
package rel
// TableDefinition interface.
type TableDefinition interface {
internalTableDefinition()
}
// Table definition.
type Table struct {
Op SchemaOp
Name string
Rename string
Definitions []TableDefinition
Optional bool
Options string
}
// Column defines a column with name and type.
func (t *Table) Column(name string, typ ColumnType, options ...ColumnOption) {
if typ == BigID || typ == ID {
options = append([]ColumnOption{Primary(true)}, options...)
}
t.Definitions = append(t.Definitions, createColumn(name, typ, options))
}
// ID defines a column with name and ID type.
// the resulting database type will depends on database.
func (t *Table) ID(name string, options ...ColumnOption) {
t.Column(name, ID, options...)
}
// BigID defines a column with name and Big ID type.
// the resulting database type will depends on database.
func (t *Table) BigID(name string, options ...ColumnOption) {
t.Column(name, BigID, options...)
}
// Bool defines a column with name and Bool type.
func (t *Table) Bool(name string, options ...ColumnOption) {
t.Column(name, Bool, options...)
}
// SmallInt defines a column with name and Small type.
func (t *Table) SmallInt(name string, options ...ColumnOption) {
t.Column(name, SmallInt, options...)
}
// Int defines a column with name and Int type.
func (t *Table) Int(name string, options ...ColumnOption) {
t.Column(name, Int, options...)
}
// BigInt defines a column with name and BigInt type.
func (t *Table) BigInt(name string, options ...ColumnOption) {
t.Column(name, BigInt, options...)
}
// Float defines a column with name and Float type.
func (t *Table) Float(name string, options ...ColumnOption) {
t.Column(name, Float, options...)
}
// Decimal defines a column with name and Decimal type.
func (t *Table) Decimal(name string, options ...ColumnOption) {
t.Column(name, Decimal, options...)
}
// String defines a column with name and String type.
func (t *Table) String(name string, options ...ColumnOption) {
t.Column(name, String, options...)
}
// Text defines a column with name and Text type.
func (t *Table) Text(name string, options ...ColumnOption) {
t.Column(name, Text, options...)
}
// JSON defines a column with name and JSON type.
func (t *Table) JSON(name string, options ...ColumnOption) {
t.Column(name, JSON, options...)
}
// Date defines a column with name and Date type.
func (t *Table) Date(name string, options ...ColumnOption) {
t.Column(name, Date, options...)
}
// DateTime defines a column with name and DateTime type.
func (t *Table) DateTime(name string, options ...ColumnOption) {
t.Column(name, DateTime, options...)
}
// Time defines a column with name and Time type.
func (t *Table) Time(name string, options ...ColumnOption) {
t.Column(name, Time, options...)
}
// PrimaryKey defines a primary key for table.
func (t *Table) PrimaryKey(column string, options ...KeyOption) {
t.PrimaryKeys([]string{column}, options...)
}
// PrimaryKeys defines composite primary keys for table.
func (t *Table) PrimaryKeys(columns []string, options ...KeyOption) {
t.Definitions = append(t.Definitions, createPrimaryKeys(columns, options))
}
// ForeignKey defines foreign key index.
func (t *Table) ForeignKey(column string, refTable string, refColumn string, options ...KeyOption) {
t.Definitions = append(t.Definitions, createForeignKey(column, refTable, refColumn, options))
}
// Unique defines an unique key for columns.
func (t *Table) Unique(columns []string, options ...KeyOption) {
t.Definitions = append(t.Definitions, createKeys(columns, UniqueKey, options))
}
// Fragment defines anything using sql fragment.
func (t *Table) Fragment(fragment string) {
t.Definitions = append(t.Definitions, Raw(fragment))
}
func (t Table) description() string {
return t.Op.String() + " table " + t.Name
}
func (t Table) internalMigration() {}
// AlterTable Migrator.
type AlterTable struct {
Table
}
// RenameColumn to a new name.
func (at *AlterTable) RenameColumn(name string, newName string, options ...ColumnOption) {
at.Definitions = append(at.Definitions, renameColumn(name, newName, options))
}
// DropColumn from this table.
func (at *AlterTable) DropColumn(name string, options ...ColumnOption) {
at.Definitions = append(at.Definitions, dropColumn(name, options))
}
func createTable(name string, options []TableOption) Table {
table := Table{
Op: SchemaCreate,
Name: name,
}
applyTableOptions(&table, options)
return table
}
func createTableIfNotExists(name string, options []TableOption) Table {
table := createTable(name, options)
table.Optional = true
return table
}
func alterTable(name string, options []TableOption) AlterTable {
table := Table{
Op: SchemaAlter,
Name: name,
}
applyTableOptions(&table, options)
return AlterTable{Table: table}
}
func renameTable(name string, newName string, options []TableOption) Table {
table := Table{
Op: SchemaRename,
Name: name,
Rename: newName,
}
applyTableOptions(&table, options)
return table
}
func dropTable(name string, options []TableOption) Table {
table := Table{
Op: SchemaDrop,
Name: name,
}
applyTableOptions(&table, options)
return table
}
func dropTableIfExists(name string, options []TableOption) Table {
table := dropTable(name, options)
table.Optional = true
return table
}

@ -0,0 +1,221 @@
package rel
import (
"fmt"
"math"
"reflect"
"strconv"
"strings"
)
func indirectInterface(rv reflect.Value) interface{} {
if rv.Kind() == reflect.Ptr {
if rv.IsNil() {
return nil
}
rv = rv.Elem()
}
return rv.Interface()
}
func indirectReflectType(rt reflect.Type) reflect.Type {
if rt.Kind() == reflect.Ptr {
return rt.Elem()
}
return rt
}
func must(err error) {
if err != nil {
panic(err)
}
}
type isZeroer interface {
IsZero() bool
}
// isZero shallowly check wether a field in struct is zero or not
func isZero(value interface{}) bool {
var (
zero bool
)
switch v := value.(type) {
case nil:
zero = true
case bool:
zero = !v
case string:
zero = v == ""
case int:
zero = v == 0
case int8:
zero = v == 0
case int16:
zero = v == 0
case int32:
zero = v == 0
case int64:
zero = v == 0
case uint:
zero = v == 0
case uint8:
zero = v == 0
case uint16:
zero = v == 0
case uint32:
zero = v == 0
case uint64:
zero = v == 0
case uintptr:
zero = v == 0
case float32:
zero = v == 0
case float64:
zero = v == 0
case isZeroer:
zero = v.IsZero()
default:
zero = isDeepZero(reflect.ValueOf(value), 0)
}
return zero
}
// modified from https://golang.org/src/reflect/value.go?s=33807:33835#L1077
func isDeepZero(rv reflect.Value, depth int) bool {
if depth < 0 {
return true
}
switch rv.Kind() {
case reflect.Bool:
return !rv.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return math.Float64bits(rv.Float()) == 0
case reflect.Complex64, reflect.Complex128:
c := rv.Complex()
return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
case reflect.Array:
// check one level deeper if it's an uuid ([16]byte)
if rv.Type().Elem().Kind() == reflect.Uint8 && rv.Len() == 16 {
depth += 1
}
for i := 0; i < rv.Len(); i++ {
if !isDeepZero(rv.Index(i), depth-1) {
return false
}
}
return true
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.UnsafePointer:
return rv.IsNil()
case reflect.Slice:
return rv.IsNil() || rv.Len() == 0
case reflect.String:
return rv.Len() == 0
case reflect.Struct:
for i := 0; i < rv.NumField(); i++ {
if !isDeepZero(rv.Field(i), depth-1) {
return false
}
}
return true
default:
return true
}
}
func setPointerValue(ft reflect.Type, fv reflect.Value, rt reflect.Type, rv reflect.Value) bool {
if ft.Elem() != rt && !rt.AssignableTo(ft.Elem()) {
return false
}
if fv.IsNil() {
fv.Set(reflect.New(ft.Elem()))
}
fv.Elem().Set(rv)
return true
}
func setConvertValue(ft reflect.Type, fv reflect.Value, rt reflect.Type, rv reflect.Value) bool {
var (
rk = rt.Kind()
fk = ft.Kind()
)
// prevents unintentional conversion
if (rk >= reflect.Int || rk <= reflect.Uint64) && fk == reflect.String {
return false
}
fv.Set(rv.Convert(ft))
return true
}
func fmtiface(v interface{}) string {
if str, ok := v.(string); ok {
return "\"" + str + "\""
}
return fmt.Sprint(v)
}
func fmtifaces(v []interface{}) string {
var str strings.Builder
for i := range v {
if i > 0 {
str.WriteString(", ")
}
str.WriteString(fmtiface(v[i]))
}
return str.String()
}
// Encode index slice into single string
func encodeIndices(indices []int) string {
var sb strings.Builder
for _, index := range indices {
sb.WriteString("/")
sb.WriteString(strconv.Itoa(index))
}
return sb.String()
}
// Get field by index and init pointers on path if flag is true
// modified from: https://cs.opensource.google/go/go/+/refs/tags/go1.17.7:src/reflect/value.go;l=1228-1245;bpv
func reflectValueFieldByIndex(rv reflect.Value, index []int, init bool) reflect.Value {
if len(index) == 1 {
return rv.Field(index[0])
}
for depth := 0; depth < len(index)-1; depth += 1 {
field := rv.Field(index[depth])
if field.Kind() != reflect.Ptr {
rv = field
continue
}
if field.IsNil() {
if !init {
targetType := field.Type().Elem().FieldByIndex(index[depth+1:]).Type
return reflect.Zero(reflect.PtrTo(targetType))
}
field.Set(reflect.New(field.Type().Elem()))
}
rv = field.Elem()
}
return rv.Field(index[len(index)-1])
}

@ -0,0 +1,12 @@
version = 1
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_root = "github.com/go-rel/sql"
[[transformers]]
name = "gofmt"
enabled = true

@ -0,0 +1,8 @@
vendor
.tool-versions
*.db
.vscode/
debug.test
.idea/
*.out
*.test

@ -0,0 +1,8 @@
builds:
- skip: true
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 REL
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,9 @@
# sql
[![GoDoc](https://godoc.org/github.com/go-rel/sql?status.svg)](https://pkg.go.dev/github.com/go-rel/sql)
[![Test](https://github.com/go-rel/sql/actions/workflows/test.yml/badge.svg)](https://github.com/go-rel/sql/actions/workflows/test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-rel/sql)](https://goreportcard.com/report/github.com/go-rel/sql)
[![codecov](https://codecov.io/gh/go-rel/sql/branch/main/graph/badge.svg?token=67b87tbq5M)](https://codecov.io/gh/go-rel/sql)
[![Gitter chat](https://badges.gitter.im/go-rel/rel.png)](https://gitter.im/go-rel/rel)
Base SQL adapter for REL.

@ -0,0 +1,33 @@
package sql
import (
"github.com/go-rel/rel"
)
type QueryBuilder interface {
Build(query rel.Query) (string, []interface{})
}
type InsertBuilder interface {
Build(table string, primaryField string, mutates map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{})
}
type InsertAllBuilder interface {
Build(table string, primaryField string, fields []string, bulkMutates []map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{})
}
type UpdateBuilder interface {
Build(table string, primaryField string, mutates map[string]rel.Mutate, filter rel.FilterQuery) (string, []interface{})
}
type DeleteBuilder interface {
Build(table string, filter rel.FilterQuery) (string, []interface{})
}
type TableBuilder interface {
Build(table rel.Table) string
}
type IndexBuilder interface {
Build(index rel.Index) string
}

@ -0,0 +1,217 @@
package builder
import (
"database/sql/driver"
"fmt"
"log"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/go-rel/sql"
)
// UnescapeCharacter disable field escaping when it starts with this character.
var UnescapeCharacter byte = '^'
var escapeCache sync.Map
type escapeCacheKey struct {
table string
value string
quoter Quoter
}
// Buffer is used to build query string.
type Buffer struct {
strings.Builder
Quoter Quoter
ValueConverter driver.ValueConverter
ArgumentPlaceholder string
ArgumentOrdinal bool
InlineValues bool
BoolTrueValue string
BoolFalseValue string
valueCount int
arguments []interface{}
}
// WriteValue query placeholder and append value to argument.
func (b *Buffer) WriteValue(value interface{}) {
if !b.InlineValues {
b.WritePlaceholder()
b.arguments = append(b.arguments, value)
return
}
// Detect float bits to not lose precision after converting to float64
var floatBits = 64
if value != nil && reflect.TypeOf(value).Kind() == reflect.Float32 {
floatBits = 32
}
if v, err := b.ValueConverter.ConvertValue(value); err != nil {
log.Printf("[WARN] unsupported inline value %v: %v", value, err)
} else {
value = v
}
if value == nil {
b.WriteString("NULL")
return
}
switch v := value.(type) {
case string:
b.WriteString(b.Quoter.Value(v))
return
case []byte:
b.WriteString(b.Quoter.Value(string(v)))
return
case time.Time:
b.WriteString(b.Quoter.Value(v.Format(sql.DefaultTimeLayout)))
return
}
rv := reflect.ValueOf(value)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
b.WriteString(strconv.FormatInt(rv.Int(), 10))
return
case reflect.Float32, reflect.Float64:
b.WriteString(strconv.FormatFloat(rv.Float(), 'g', -1, floatBits))
return
case reflect.Bool:
if rv.Bool() {
b.WriteString(b.BoolTrueValue)
} else {
b.WriteString(b.BoolFalseValue)
}
return
}
b.WriteString(fmt.Sprintf("%v", value))
}
// WritePlaceholder without adding argument.
// argument can be added later using AddArguments function.
func (b *Buffer) WritePlaceholder() {
b.valueCount++
b.WriteString(b.ArgumentPlaceholder)
if b.ArgumentOrdinal {
b.WriteString(strconv.Itoa(b.valueCount))
}
}
// WriteField writes table and field name.
func (b *Buffer) WriteField(table, field string) {
b.WriteString(b.escape(table, field))
}
// WriteEscape string.
func (b *Buffer) WriteEscape(value string) {
b.WriteString(b.escape("", value))
}
func (b Buffer) escape(table, value string) string {
if table == "" && value == "*" {
return value
}
key := escapeCacheKey{table: table, value: value, quoter: b.Quoter}
escapedValue, ok := escapeCache.Load(key)
if ok {
return escapedValue.(string)
}
var escaped_table string
if table != "" {
if strings.IndexByte(table, '.') >= 0 {
parts := strings.Split(table, ".")
for i, part := range parts {
part = strings.TrimSpace(part)
parts[i] = b.Quoter.ID(part)
}
escaped_table = strings.Join(parts, ".")
} else {
escaped_table = b.Quoter.ID(table)
}
}
if value == "*" {
escapedValue = escaped_table + ".*"
} else if len(value) > 0 && value[0] == UnescapeCharacter {
escapedValue = value[1:]
} else if _, err := strconv.Atoi(value); err == nil {
escapedValue = value
} else if i := strings.Index(strings.ToLower(value), " as "); i > -1 {
escapedValue = b.escape(table, value[:i]) + " AS " + b.Quoter.ID(value[i+4:])
} else if start, end := strings.IndexRune(value, '('), strings.IndexRune(value, ')'); start >= 0 && end >= 0 && end > start {
escapedValue = value[:start+1] + b.escape(table, value[start+1:end]) + value[end:]
} else {
parts := strings.Split(value, ".")
for i, part := range parts {
part = strings.TrimSpace(part)
if part == "*" && i == len(parts)-1 {
break
}
parts[i] = b.Quoter.ID(part)
}
result := strings.Join(parts, ".")
if len(parts) == 1 && table != "" {
result = escaped_table + "." + result
}
escapedValue = result
}
escapeCache.Store(key, escapedValue)
return escapedValue.(string)
}
// AddArguments appends multiple arguments without writing placeholder query..
func (b *Buffer) AddArguments(args ...interface{}) {
if b.arguments == nil {
b.arguments = args
} else {
b.arguments = append(b.arguments, args...)
}
}
func (b Buffer) Arguments() []interface{} {
return b.arguments
}
// Reset buffer.
func (b *Buffer) Reset() {
b.Builder.Reset()
b.valueCount = 0
b.arguments = nil
}
// BufferFactory is used to create buffer based on shared settings.
type BufferFactory struct {
Quoter Quoter
ValueConverter driver.ValueConverter
ArgumentPlaceholder string
ArgumentOrdinal bool
InlineValues bool
BoolTrueValue string
BoolFalseValue string
}
func (bf BufferFactory) Create() Buffer {
conv := bf.ValueConverter
if conv == nil {
conv = driver.DefaultParameterConverter
}
return Buffer{
Quoter: bf.Quoter,
ValueConverter: conv,
ArgumentPlaceholder: bf.ArgumentPlaceholder,
ArgumentOrdinal: bf.ArgumentOrdinal,
InlineValues: bf.InlineValues,
BoolTrueValue: bf.BoolTrueValue,
BoolFalseValue: bf.BoolFalseValue,
}
}

@ -0,0 +1,31 @@
package builder
import (
"github.com/go-rel/rel"
)
// Delete builder.
type Delete struct {
BufferFactory BufferFactory
Query QueryWriter
Filter Filter
}
// Build SQL query and its arguments.
func (ds Delete) Build(table string, filter rel.FilterQuery) (string, []interface{}) {
var (
buffer = ds.BufferFactory.Create()
)
buffer.WriteString("DELETE FROM ")
buffer.WriteEscape(table)
if !filter.None() {
buffer.WriteString(" WHERE ")
ds.Filter.Write(&buffer, table, filter, ds.Query)
}
buffer.WriteString(";")
return buffer.String(), buffer.Arguments()
}

@ -0,0 +1,157 @@
package builder
import (
"github.com/go-rel/rel"
)
// Filter builder.
type Filter struct{}
// Write SQL to buffer.
func (f Filter) Write(buffer *Buffer, table string, filter rel.FilterQuery, queryWriter QueryWriter) {
switch filter.Type {
case rel.FilterAndOp:
f.WriteLogical(buffer, table, "AND", filter.Inner, queryWriter)
case rel.FilterOrOp:
f.WriteLogical(buffer, table, "OR", filter.Inner, queryWriter)
case rel.FilterNotOp:
buffer.WriteString("NOT ")
f.WriteLogical(buffer, table, "AND", filter.Inner, queryWriter)
case rel.FilterEqOp,
rel.FilterNeOp,
rel.FilterLtOp,
rel.FilterLteOp,
rel.FilterGtOp,
rel.FilterGteOp:
f.WriteComparison(buffer, table, filter, queryWriter)
case rel.FilterNilOp:
buffer.WriteField(table, filter.Field)
buffer.WriteString(" IS NULL")
case rel.FilterNotNilOp:
buffer.WriteField(table, filter.Field)
buffer.WriteString(" IS NOT NULL")
case rel.FilterInOp,
rel.FilterNinOp:
f.WriteInclusion(buffer, table, filter, queryWriter)
case rel.FilterLikeOp:
buffer.WriteField(table, filter.Field)
buffer.WriteString(" LIKE ")
buffer.WriteValue(filter.Value)
case rel.FilterNotLikeOp:
buffer.WriteField(table, filter.Field)
buffer.WriteString(" NOT LIKE ")
buffer.WriteValue(filter.Value)
case rel.FilterFragmentOp:
buffer.WriteString(filter.Field)
if !buffer.InlineValues {
buffer.AddArguments(filter.Value.([]interface{})...)
}
}
}
// WriteLogical SQL to buffer.
func (f Filter) WriteLogical(buffer *Buffer, table, op string, inner []rel.FilterQuery, queryWriter QueryWriter) {
var (
length = len(inner)
)
if length > 1 {
buffer.WriteByte('(')
}
for i, c := range inner {
f.Write(buffer, table, c, queryWriter)
if i < length-1 {
buffer.WriteByte(' ')
buffer.WriteString(op)
buffer.WriteByte(' ')
}
}
if length > 1 {
buffer.WriteByte(')')
}
}
// WriteComparison SQL to buffer.
func (f Filter) WriteComparison(buffer *Buffer, table string, filter rel.FilterQuery, queryWriter QueryWriter) {
buffer.WriteField(table, filter.Field)
switch filter.Type {
case rel.FilterEqOp:
buffer.WriteByte('=')
case rel.FilterNeOp:
buffer.WriteString("<>")
case rel.FilterLtOp:
buffer.WriteByte('<')
case rel.FilterLteOp:
buffer.WriteString("<=")
case rel.FilterGtOp:
buffer.WriteByte('>')
case rel.FilterGteOp:
buffer.WriteString(">=")
}
switch v := filter.Value.(type) {
case rel.SubQuery:
// For warped sub-queries
f.WriteSubQuery(buffer, v, queryWriter)
case rel.Query:
// For sub-queries without warp
f.WriteSubQuery(buffer, rel.SubQuery{Query: v}, queryWriter)
default:
// For simple values
buffer.WriteValue(filter.Value)
}
}
// WriteInclusion SQL to buffer.
func (f Filter) WriteInclusion(buffer *Buffer, table string, filter rel.FilterQuery, queryWriter QueryWriter) {
var (
values = filter.Value.([]interface{})
)
if len(values) == 0 {
if filter.Type == rel.FilterInOp {
buffer.WriteString("1=0")
} else {
buffer.WriteString("1=1")
}
} else {
buffer.WriteField(table, filter.Field)
if filter.Type == rel.FilterInOp {
buffer.WriteString(" IN ")
} else {
buffer.WriteString(" NOT IN ")
}
f.WriteInclusionValues(buffer, values, queryWriter)
}
}
func (f Filter) WriteInclusionValues(buffer *Buffer, values []interface{}, queryWriter QueryWriter) {
if len(values) == 1 {
if value, ok := values[0].(rel.Query); ok {
f.WriteSubQuery(buffer, rel.SubQuery{Query: value}, queryWriter)
return
}
}
buffer.WriteByte('(')
for i := 0; i < len(values); i++ {
if i > 0 {
buffer.WriteByte(',')
}
buffer.WriteValue(values[i])
}
buffer.WriteByte(')')
}
func (f Filter) WriteSubQuery(buffer *Buffer, sub rel.SubQuery, queryWriter QueryWriter) {
buffer.WriteString(sub.Prefix)
buffer.WriteByte('(')
queryWriter.Write(buffer, sub.Query)
buffer.WriteByte(')')
}

@ -0,0 +1,93 @@
package builder
import (
"log"
"github.com/go-rel/rel"
)
// Index builder.
type Index struct {
BufferFactory BufferFactory
Query QueryWriter
Filter Filter
DropIndexOnTable bool
SupportFilter bool
}
// Build sql query for index.
func (i Index) Build(index rel.Index) string {
buffer := i.BufferFactory.Create()
switch index.Op {
case rel.SchemaCreate:
i.WriteCreateIndex(&buffer, index)
case rel.SchemaDrop:
i.WriteDropIndex(&buffer, index)
}
i.WriteOptions(&buffer, index.Options)
buffer.WriteByte(';')
return buffer.String()
}
// WriteCreateIndex to buffer
func (i Index) WriteCreateIndex(buffer *Buffer, index rel.Index) {
buffer.WriteString("CREATE ")
if index.Unique {
buffer.WriteString("UNIQUE ")
}
buffer.WriteString("INDEX ")
if index.Optional {
buffer.WriteString("IF NOT EXISTS ")
}
buffer.WriteEscape(index.Name)
buffer.WriteString(" ON ")
buffer.WriteEscape(index.Table)
buffer.WriteString(" (")
for n, col := range index.Columns {
if n > 0 {
buffer.WriteString(", ")
}
buffer.WriteEscape(col)
}
buffer.WriteString(")")
if !index.Filter.None() {
if !i.SupportFilter {
log.Print("[REL] Adapter does not support filtered/partial indexes")
return
}
buffer.WriteString(" WHERE ")
i.Filter.Write(buffer, "", index.Filter, i.Query)
}
}
// WriteDropIndex to buffer
func (i Index) WriteDropIndex(buffer *Buffer, index rel.Index) {
buffer.WriteString("DROP INDEX ")
if index.Optional {
buffer.WriteString("IF EXISTS ")
}
buffer.WriteEscape(index.Name)
if i.DropIndexOnTable {
buffer.WriteString(" ON ")
buffer.WriteEscape(index.Table)
}
}
// WriteOptions sql to buffer.
func (i Index) WriteOptions(buffer *Buffer, options string) {
if options == "" {
return
}
buffer.WriteByte(' ')
buffer.WriteString(options)
}

@ -0,0 +1,82 @@
package builder
import (
"github.com/go-rel/rel"
)
// Insert builder.
type Insert struct {
BufferFactory BufferFactory
ReturningPrimaryValue bool
InsertDefaultValues bool
OnConflict OnConflict
}
// Build sql query and its arguments.
func (i Insert) Build(table string, primaryField string, mutates map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{}) {
var (
buffer = i.BufferFactory.Create()
)
i.WriteInsertInto(&buffer, table)
i.WriteValues(&buffer, mutates)
i.OnConflict.WriteMutates(&buffer, mutates, onConflict)
i.WriteReturning(&buffer, primaryField)
buffer.WriteString(";")
return buffer.String(), buffer.Arguments()
}
func (i Insert) WriteInsertInto(buffer *Buffer, table string) {
buffer.WriteString("INSERT INTO ")
buffer.WriteEscape(table)
}
func (i Insert) WriteValues(buffer *Buffer, mutates map[string]rel.Mutate) {
var (
count = len(mutates)
)
if count == 0 && i.InsertDefaultValues {
buffer.WriteString(" DEFAULT VALUES")
} else {
buffer.WriteString(" (")
var (
n = 0
arguments = make([]interface{}, 0, count)
)
for field, mut := range mutates {
if mut.Type == rel.ChangeSetOp {
if n > 0 {
buffer.WriteByte(',')
}
buffer.WriteEscape(field)
arguments = append(arguments, mut.Value)
n++
}
}
buffer.WriteString(") VALUES (")
for i := range arguments {
if i > 0 {
buffer.WriteByte(',')
}
buffer.WritePlaceholder()
}
buffer.AddArguments(arguments...)
buffer.WriteByte(')')
}
}
func (i Insert) WriteReturning(buffer *Buffer, primaryField string) {
if i.ReturningPrimaryValue && primaryField != "" {
buffer.WriteString(" RETURNING ")
buffer.WriteEscape(primaryField)
}
}

@ -0,0 +1,80 @@
package builder
import (
"github.com/go-rel/rel"
)
// InsertAll builder.
type InsertAll struct {
BufferFactory BufferFactory
ReturningPrimaryValue bool
OnConflict OnConflict
}
// Build SQL string and its arguments.
func (ia InsertAll) Build(table string, primaryField string, fields []string, bulkMutates []map[string]rel.Mutate, onConflict rel.OnConflict) (string, []interface{}) {
var (
buffer = ia.BufferFactory.Create()
)
ia.WriteInsertInto(&buffer, table)
ia.WriteValues(&buffer, fields, bulkMutates)
ia.OnConflict.Write(&buffer, fields, onConflict)
ia.WriteReturning(&buffer, primaryField)
buffer.WriteString(";")
return buffer.String(), buffer.Arguments()
}
func (ia InsertAll) WriteInsertInto(buffer *Buffer, table string) {
buffer.WriteString("INSERT INTO ")
buffer.WriteEscape(table)
}
func (ia InsertAll) WriteValues(buffer *Buffer, fields []string, bulkMutates []map[string]rel.Mutate) {
var (
fieldsCount = len(fields)
mutatesCount = len(bulkMutates)
)
buffer.WriteString(" (")
for i := range fields {
buffer.WriteEscape(fields[i])
if i < fieldsCount-1 {
buffer.WriteByte(',')
}
}
buffer.WriteString(") VALUES ")
for i, mutates := range bulkMutates {
buffer.WriteByte('(')
for j, field := range fields {
if mut, ok := mutates[field]; ok && mut.Type == rel.ChangeSetOp {
buffer.WriteValue(mut.Value)
} else {
buffer.WriteString("DEFAULT")
}
if j < fieldsCount-1 {
buffer.WriteByte(',')
}
}
if i < mutatesCount-1 {
buffer.WriteString("),")
} else {
buffer.WriteByte(')')
}
}
}
func (ia InsertAll) WriteReturning(buffer *Buffer, primaryField string) {
if ia.ReturningPrimaryValue && primaryField != "" {
buffer.WriteString(" RETURNING ")
buffer.WriteEscape(primaryField)
}
}

@ -0,0 +1,100 @@
package builder
import (
"github.com/go-rel/rel"
)
type OnConflict struct {
Statement string
IgnoreStatement string
UpdateStatement string
TableQualifier string
SupportKey bool
UseValues bool
}
func (oc OnConflict) Write(buffer *Buffer, fields []string, onConflict rel.OnConflict) {
if onConflict.Keys == nil && onConflict.Fragment == "" {
return
}
buffer.WriteByte(' ')
buffer.WriteString(oc.Statement)
oc.WriteKeys(buffer, onConflict)
buffer.WriteByte(' ')
switch {
case onConflict.Ignore:
oc.WriteIgnore(buffer, fields)
case onConflict.Replace:
oc.WriteReplace(buffer, fields)
case onConflict.Fragment != "":
buffer.WriteString(onConflict.Fragment)
buffer.AddArguments(onConflict.FragmentArgs...)
}
}
func (oc OnConflict) WriteMutates(buffer *Buffer, mutates map[string]rel.Mutate, onConflict rel.OnConflict) {
var fields []string
if onConflict.Replace || (onConflict.Ignore && oc.IgnoreStatement == "") {
fields = make([]string, len(mutates))
i := 0
for field := range mutates {
fields[i] = field
i++
}
}
oc.Write(buffer, fields, onConflict)
}
func (oc OnConflict) WriteKeys(buffer *Buffer, onConflict rel.OnConflict) {
if !oc.SupportKey || len(onConflict.Keys) == 0 {
return
}
buffer.WriteByte('(')
for i := range onConflict.Keys {
if i > 0 {
buffer.WriteByte(',')
}
buffer.WriteEscape(onConflict.Keys[i])
}
buffer.WriteByte(')')
}
func (oc OnConflict) WriteIgnore(buffer *Buffer, fields []string) {
if oc.IgnoreStatement == "" && len(fields) != 0 {
// mysql specific
buffer.WriteString(oc.UpdateStatement)
buffer.WriteByte(' ')
buffer.WriteEscape(fields[0])
buffer.WriteByte('=')
buffer.WriteEscape(fields[0])
} else {
buffer.WriteString(oc.IgnoreStatement)
}
}
func (oc OnConflict) WriteReplace(buffer *Buffer, fields []string) {
buffer.WriteString(oc.UpdateStatement)
buffer.WriteByte(' ')
for i, field := range fields {
if i > 0 {
buffer.WriteByte(',')
}
buffer.WriteEscape(field)
buffer.WriteByte('=')
if oc.UseValues {
buffer.WriteString("VALUES(")
buffer.WriteEscape(field)
buffer.WriteByte(')')
} else {
buffer.WriteField(oc.TableQualifier, field)
}
i++
}
}

@ -0,0 +1,211 @@
package builder
import (
"strconv"
"strings"
"github.com/go-rel/rel"
)
type QueryWriter interface {
Write(buffer *Buffer, query rel.Query)
}
// Query builder.
type Query struct {
BufferFactory BufferFactory
Filter Filter
}
// Build SQL string and it arguments.
func (q Query) Build(query rel.Query) (string, []interface{}) {
var (
buffer = q.BufferFactory.Create()
)
q.Write(&buffer, query)
return buffer.String(), buffer.Arguments()
}
// Write SQL to buffer.
func (q Query) Write(buffer *Buffer, query rel.Query) {
if query.SQLQuery.Statement != "" {
buffer.WriteString(query.SQLQuery.Statement)
buffer.AddArguments(query.SQLQuery.Values...)
return
}
rootQuery := buffer.Len() == 0
q.WriteSelect(buffer, query.Table, query.SelectQuery)
q.WriteQuery(buffer, query)
if rootQuery {
buffer.WriteByte(';')
}
}
// WriteSelect SQL to buffer.
func (q Query) WriteSelect(buffer *Buffer, table string, selectQuery rel.SelectQuery) {
if len(selectQuery.Fields) == 0 {
buffer.WriteString("SELECT ")
if selectQuery.OnlyDistinct {
buffer.WriteString("DISTINCT ")
}
buffer.WriteField(table, "*")
return
}
buffer.WriteString("SELECT ")
if selectQuery.OnlyDistinct {
buffer.WriteString("DISTINCT ")
}
l := len(selectQuery.Fields) - 1
for i, f := range selectQuery.Fields {
buffer.WriteField(table, f)
if i < l {
buffer.WriteByte(',')
}
}
}
// WriteQuery SQL to buffer.
func (q Query) WriteQuery(buffer *Buffer, query rel.Query) {
q.WriteFrom(buffer, query.Table)
q.WriteJoin(buffer, query.Table, query.JoinQuery)
q.WriteWhere(buffer, query.Table, query.WhereQuery)
if len(query.GroupQuery.Fields) > 0 {
q.WriteGroupBy(buffer, query.Table, query.GroupQuery.Fields)
q.WriteHaving(buffer, query.Table, query.GroupQuery.Filter)
}
q.WriteOrderBy(buffer, query.Table, query.SortQuery)
q.WriteLimitOffet(buffer, query.LimitQuery, query.OffsetQuery)
if query.LockQuery != "" {
buffer.WriteByte(' ')
buffer.WriteString(string(query.LockQuery))
}
}
// WriteFrom SQL to buffer.
func (q Query) WriteFrom(buffer *Buffer, table string) {
buffer.WriteString(" FROM ")
buffer.WriteEscape(table)
}
// WriteJoin SQL to buffer.
func (q Query) WriteJoin(buffer *Buffer, table string, joins []rel.JoinQuery) {
if len(joins) == 0 {
return
}
for _, join := range joins {
var (
from = join.From
to = join.To
)
// TODO: move this to core functionality, and infer join condition using assoc data.
if join.Arguments == nil && (join.From == "" || join.To == "") {
from = table + "." + strings.TrimSuffix(join.Table, "s") + "_id"
to = join.Table + ".id"
}
buffer.WriteByte(' ')
buffer.WriteString(join.Mode)
buffer.WriteByte(' ')
if join.Table != "" {
buffer.WriteEscape(join.Table)
buffer.WriteString(" ON ")
buffer.WriteEscape(from)
buffer.WriteString("=")
buffer.WriteEscape(to)
if !join.Filter.None() {
buffer.WriteString(" AND ")
q.Filter.Write(buffer, join.Table, join.Filter, q)
}
}
buffer.AddArguments(join.Arguments...)
}
}
// WriteWhere SQL to buffer.
func (q Query) WriteWhere(buffer *Buffer, table string, filter rel.FilterQuery) {
if filter.None() {
return
}
buffer.WriteString(" WHERE ")
q.Filter.Write(buffer, table, filter, q)
}
// WriteGroupBy SQL to buffer.
func (q Query) WriteGroupBy(buffer *Buffer, table string, fields []string) {
buffer.WriteString(" GROUP BY ")
l := len(fields) - 1
for i, f := range fields {
buffer.WriteField(table, f)
if i < l {
buffer.WriteByte(',')
}
}
}
// WriteHaving SQL to buffer.
func (q Query) WriteHaving(buffer *Buffer, table string, filter rel.FilterQuery) {
if filter.None() {
return
}
buffer.WriteString(" HAVING ")
q.Filter.Write(buffer, table, filter, q)
}
// WriteOrderBy SQL to buffer.
func (q Query) WriteOrderBy(buffer *Buffer, table string, orders []rel.SortQuery) {
var (
length = len(orders)
)
if length == 0 {
return
}
buffer.WriteString(" ORDER BY ")
for i, order := range orders {
if i > 0 {
buffer.WriteString(", ")
}
buffer.WriteField(table, order.Field)
if order.Asc() {
buffer.WriteString(" ASC")
} else {
buffer.WriteString(" DESC")
}
}
}
// WriteLimitOffet SQL to buffer.
func (q Query) WriteLimitOffet(buffer *Buffer, limit rel.Limit, offset rel.Offset) {
if limit > 0 {
buffer.WriteString(" LIMIT ")
buffer.WriteString(strconv.Itoa(int(limit)))
if offset > 0 {
buffer.WriteString(" OFFSET ")
buffer.WriteString(strconv.Itoa(int(offset)))
}
}
}

@ -0,0 +1,42 @@
package builder
import (
"strings"
)
// Quoter returns safe and valid SQL strings to use when building a SQL text.
type Quoter interface {
// ID quotes identifiers such as schema, table, or column names.
// ID does not operate on multipart identifiers such as "public.Table",
// it only operates on single identifiers such as "public" and "Table".
ID(name string) string
// Value quotes database values such as string or []byte types as strings
// that are suitable and safe to embed in SQL text. The returned value
// of a string will include all surrounding quotes.
//
// If a value type is not supported it must panic.
Value(v interface{}) string
}
// Quote is default implementation of Quoter interface.
type Quote struct {
IDPrefix string
IDSuffix string
IDSuffixEscapeChar string
ValueQuote string
ValueQuoteEscapeChar string
}
func (q Quote) ID(name string) string {
return q.IDPrefix + strings.ReplaceAll(name, q.IDSuffix, q.IDSuffixEscapeChar+q.IDSuffix) + q.IDSuffix
}
func (q Quote) Value(v interface{}) string {
switch v := v.(type) {
default:
panic("unsupported value")
case string:
return q.ValueQuote + strings.ReplaceAll(v, q.ValueQuote, q.ValueQuoteEscapeChar+q.ValueQuote) + q.ValueQuote
}
}

@ -0,0 +1,230 @@
package builder
import (
"strconv"
"github.com/go-rel/rel"
)
type ColumnMapper func(*rel.Column) (string, int, int)
// Table builder.
type Table struct {
BufferFactory BufferFactory
ColumnMapper ColumnMapper
}
// Build SQL query for table creation and modification.
func (t Table) Build(table rel.Table) string {
var (
buffer = t.BufferFactory.Create()
)
switch table.Op {
case rel.SchemaCreate:
t.WriteCreateTable(&buffer, table)
case rel.SchemaAlter:
t.WriteAlterTable(&buffer, table)
case rel.SchemaRename:
t.WriteRenameTable(&buffer, table)
case rel.SchemaDrop:
t.WriteDropTable(&buffer, table)
}
return buffer.String()
}
// WriteCreateTable query to buffer.
func (t Table) WriteCreateTable(buffer *Buffer, table rel.Table) {
buffer.WriteString("CREATE TABLE ")
if table.Optional {
buffer.WriteString("IF NOT EXISTS ")
}
buffer.WriteEscape(table.Name)
if len(table.Definitions) > 0 {
buffer.WriteString(" (")
for i, def := range table.Definitions {
if i > 0 {
buffer.WriteString(", ")
}
switch v := def.(type) {
case rel.Column:
t.WriteColumn(buffer, v)
case rel.Key:
t.WriteKey(buffer, v)
case rel.Raw:
buffer.WriteString(string(v))
}
}
buffer.WriteByte(')')
}
t.WriteOptions(buffer, table.Options)
buffer.WriteByte(';')
}
// WriteAlterTable query to buffer.
func (t Table) WriteAlterTable(buffer *Buffer, table rel.Table) {
for _, def := range table.Definitions {
buffer.WriteString("ALTER TABLE ")
buffer.WriteEscape(table.Name)
buffer.WriteByte(' ')
switch v := def.(type) {
case rel.Column:
switch v.Op {
case rel.SchemaCreate:
buffer.WriteString("ADD COLUMN ")
t.WriteColumn(buffer, v)
case rel.SchemaRename:
// Add Change
buffer.WriteString("RENAME COLUMN ")
buffer.WriteEscape(v.Name)
buffer.WriteString(" TO ")
buffer.WriteEscape(v.Rename)
case rel.SchemaDrop:
buffer.WriteString("DROP COLUMN ")
buffer.WriteEscape(v.Name)
}
case rel.Key:
// TODO: Rename and Drop, PR welcomed.
switch v.Op {
case rel.SchemaCreate:
buffer.WriteString("ADD ")
t.WriteKey(buffer, v)
}
}
t.WriteOptions(buffer, table.Options)
buffer.WriteByte(';')
}
}
// WriteRenameTable query to buffer.
func (t Table) WriteRenameTable(buffer *Buffer, table rel.Table) {
buffer.WriteString("ALTER TABLE ")
buffer.WriteEscape(table.Name)
buffer.WriteString(" RENAME TO ")
buffer.WriteEscape(table.Rename)
buffer.WriteByte(';')
}
// WriteDropTable query to buffer.
func (t Table) WriteDropTable(buffer *Buffer, table rel.Table) {
buffer.WriteString("DROP TABLE ")
if table.Optional {
buffer.WriteString("IF EXISTS ")
}
buffer.WriteEscape(table.Name)
buffer.WriteByte(';')
}
// WriteColumn definition to buffer.
func (t Table) WriteColumn(buffer *Buffer, column rel.Column) {
var (
typ, m, n = t.ColumnMapper(&column)
)
buffer.WriteEscape(column.Name)
buffer.WriteByte(' ')
buffer.WriteString(typ)
if m != 0 {
buffer.WriteByte('(')
buffer.WriteString(strconv.Itoa(m))
if n != 0 {
buffer.WriteByte(',')
buffer.WriteString(strconv.Itoa(n))
}
buffer.WriteByte(')')
}
if column.Unsigned {
buffer.WriteString(" UNSIGNED")
}
if column.Unique {
buffer.WriteString(" UNIQUE")
}
if column.Required {
buffer.WriteString(" NOT NULL")
}
if column.Primary {
buffer.WriteString(" PRIMARY KEY")
}
if column.Default != nil {
buffer.WriteString(" DEFAULT ")
buffer.WriteValue(column.Default)
}
t.WriteOptions(buffer, column.Options)
}
// WriteKey definition to buffer.
func (t Table) WriteKey(buffer *Buffer, key rel.Key) {
var (
typ = string(key.Type)
)
buffer.WriteString(typ)
if key.Name != "" {
buffer.WriteByte(' ')
buffer.WriteEscape(key.Name)
}
buffer.WriteString(" (")
for i, col := range key.Columns {
if i > 0 {
buffer.WriteString(", ")
}
buffer.WriteEscape(col)
}
buffer.WriteString(")")
if key.Type == rel.ForeignKey {
buffer.WriteString(" REFERENCES ")
buffer.WriteEscape(key.Reference.Table)
buffer.WriteString(" (")
for i, col := range key.Reference.Columns {
if i > 0 {
buffer.WriteString(", ")
}
buffer.WriteEscape(col)
}
buffer.WriteString(")")
if onDelete := key.Reference.OnDelete; onDelete != "" {
buffer.WriteString(" ON DELETE ")
buffer.WriteString(onDelete)
}
if onUpdate := key.Reference.OnUpdate; onUpdate != "" {
buffer.WriteString(" ON UPDATE ")
buffer.WriteString(onUpdate)
}
}
t.WriteOptions(buffer, key.Options)
}
// WriteOptions sql to buffer.
func (t Table) WriteOptions(buffer *Buffer, options string) {
if options == "" {
return
}
buffer.WriteByte(' ')
buffer.WriteString(options)
}

@ -0,0 +1,60 @@
package builder
import (
"github.com/go-rel/rel"
)
// Update builder.
type Update struct {
BufferFactory BufferFactory
Query QueryWriter
Filter Filter
}
// Build SQL string and it arguments.
func (u Update) Build(table string, primaryField string, mutates map[string]rel.Mutate, filter rel.FilterQuery) (string, []interface{}) {
var (
buffer = u.BufferFactory.Create()
)
buffer.WriteString("UPDATE ")
buffer.WriteEscape(table)
buffer.WriteString(" SET ")
i := 0
for field, mut := range mutates {
if field == primaryField {
continue
}
if i > 0 {
buffer.WriteByte(',')
}
i++
switch mut.Type {
case rel.ChangeSetOp:
buffer.WriteEscape(field)
buffer.WriteByte('=')
buffer.WriteValue(mut.Value)
case rel.ChangeIncOp:
buffer.WriteEscape(field)
buffer.WriteByte('=')
buffer.WriteEscape(field)
buffer.WriteByte('+')
buffer.WriteValue(mut.Value)
case rel.ChangeFragmentOp:
buffer.WriteString(field)
buffer.AddArguments(mut.Value.([]interface{})...)
}
}
if !filter.None() {
buffer.WriteString(" WHERE ")
u.Filter.Write(&buffer, table, filter, u.Query)
}
buffer.WriteString(";")
return buffer.String(), buffer.Arguments()
}

@ -0,0 +1,20 @@
package sql
import (
"database/sql"
)
// Cursor used for retrieving result.
type Cursor struct {
*sql.Rows
}
// Fields returned in the result.
func (c *Cursor) Fields() ([]string, error) {
return c.Columns()
}
// NopScanner for this adapter.
func (c *Cursor) NopScanner() interface{} {
return &sql.RawBytes{}
}

302
vendor/github.com/go-rel/sql/sql.go generated vendored

@ -0,0 +1,302 @@
package sql
import (
"context"
"database/sql"
"errors"
"strconv"
"github.com/go-rel/rel"
)
// ErrorMapper function.
type ErrorMapper func(error) error
// IncrementFunc function.
type IncrementFunc func(SQL) int
// SQL base adapter.
type SQL struct {
QueryBuilder QueryBuilder
InsertBuilder InsertBuilder
InsertAllBuilder InsertAllBuilder
UpdateBuilder UpdateBuilder
DeleteBuilder DeleteBuilder
TableBuilder TableBuilder
IndexBuilder IndexBuilder
IncrementFunc IncrementFunc
ErrorMapper ErrorMapper
DB *sql.DB
Tx *sql.Tx
Savepoint int
Instrumenter rel.Instrumenter
}
// Instrumentation set instrumenter for this adapter.
func (s *SQL) Instrumentation(instrumenter rel.Instrumenter) {
s.Instrumenter = instrumenter
}
// DoExec using active database connection.
func (s SQL) DoExec(ctx context.Context, statement string, args []interface{}) (sql.Result, error) {
var (
err error
result sql.Result
finish = s.Instrumenter.Observe(ctx, "adapter-exec", statement)
)
if s.Tx != nil {
result, err = s.Tx.ExecContext(ctx, statement, args...)
} else {
result, err = s.DB.ExecContext(ctx, statement, args...)
}
finish(err)
return result, err
}
// DoQuery using active database connection.
func (s SQL) DoQuery(ctx context.Context, statement string, args []interface{}) (*sql.Rows, error) {
var (
err error
rows *sql.Rows
)
finish := s.Instrumenter.Observe(ctx, "adapter-query", statement)
if s.Tx != nil {
rows, err = s.Tx.QueryContext(ctx, statement, args...)
} else {
rows, err = s.DB.QueryContext(ctx, statement, args...)
}
finish(err)
return rows, err
}
// Begin begins a new transaction.
func (s SQL) Begin(ctx context.Context) (rel.Adapter, error) {
var (
tx *sql.Tx
savepoint int
err error
)
finish := s.Instrumenter.Observe(ctx, "adapter-begin", "begin transaction")
if s.Tx != nil {
tx = s.Tx
savepoint = s.Savepoint + 1
_, err = s.Tx.ExecContext(ctx, "SAVEPOINT s"+strconv.Itoa(savepoint)+";")
} else {
tx, err = s.DB.BeginTx(ctx, nil)
}
finish(err)
return &SQL{
QueryBuilder: s.QueryBuilder,
InsertBuilder: s.InsertBuilder,
InsertAllBuilder: s.InsertAllBuilder,
UpdateBuilder: s.UpdateBuilder,
DeleteBuilder: s.DeleteBuilder,
TableBuilder: s.TableBuilder,
IndexBuilder: s.IndexBuilder,
IncrementFunc: s.IncrementFunc,
ErrorMapper: s.ErrorMapper,
Tx: tx,
Savepoint: savepoint,
Instrumenter: s.Instrumenter,
}, s.ErrorMapper(err)
}
// Commit commits current transaction.
func (s SQL) Commit(ctx context.Context) error {
var err error
finish := s.Instrumenter.Observe(ctx, "adapter-commit", "commit transaction")
if s.Tx == nil {
err = errors.New("unable to commit outside transaction")
} else if s.Savepoint > 0 {
_, err = s.Tx.ExecContext(ctx, "RELEASE SAVEPOINT s"+strconv.Itoa(s.Savepoint)+";")
} else {
err = s.Tx.Commit()
}
finish(err)
return s.ErrorMapper(err)
}
// Rollback revert current transaction.
func (s SQL) Rollback(ctx context.Context) error {
var err error
finish := s.Instrumenter.Observe(ctx, "adapter-rollback", "rollback transaction")
if s.Tx == nil {
err = errors.New("unable to rollback outside transaction")
} else if s.Savepoint > 0 {
_, err = s.Tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT s"+strconv.Itoa(s.Savepoint)+";")
} else {
err = s.Tx.Rollback()
}
finish(err)
return s.ErrorMapper(err)
}
// Ping database.
func (s SQL) Ping(ctx context.Context) error {
return s.DB.PingContext(ctx)
}
// Close database connection.
//
// TODO: add closer to adapter interface
func (s SQL) Close() error {
return s.DB.Close()
}
// Query performs query operation.
func (s SQL) Query(ctx context.Context, query rel.Query) (rel.Cursor, error) {
var (
statement, args = s.QueryBuilder.Build(query)
rows, err = s.DoQuery(ctx, statement, args)
)
return &Cursor{Rows: rows}, s.ErrorMapper(err)
}
// Exec performs exec operation.
func (s SQL) Exec(ctx context.Context, statement string, args []interface{}) (int64, int64, error) {
var (
res, err = s.DoExec(ctx, statement, args)
)
if err != nil {
return 0, 0, s.ErrorMapper(err)
}
lastID, _ := res.LastInsertId()
rowCount, _ := res.RowsAffected()
return lastID, rowCount, nil
}
// Aggregate record using given query.
func (s SQL) Aggregate(ctx context.Context, query rel.Query, mode string, field string) (int, error) {
var (
out sql.NullInt64
aggregateField = "^" + mode + "(" + field + ") AS result"
aggregateQuery = query.Select(append([]string{aggregateField}, query.GroupQuery.Fields...)...)
statement, args = s.QueryBuilder.Build(aggregateQuery)
rows, err = s.DoQuery(ctx, statement, args)
)
defer rows.Close()
if err == nil && rows.Next() {
rows.Scan(&out)
}
return int(out.Int64), s.ErrorMapper(err)
}
// Insert inserts a record to database and returns its id.
func (s SQL) Insert(ctx context.Context, query rel.Query, primaryField string, mutates map[string]rel.Mutate, onConflict rel.OnConflict) (interface{}, error) {
var (
statement, args = s.InsertBuilder.Build(query.Table, primaryField, mutates, onConflict)
id, _, err = s.Exec(ctx, statement, args)
)
return id, err
}
// InsertAll inserts multiple records to database and returns its ids.
func (s SQL) InsertAll(ctx context.Context, query rel.Query, primaryField string, fields []string, bulkMutates []map[string]rel.Mutate, onConflict rel.OnConflict) ([]interface{}, error) {
var (
statement, args = s.InsertAllBuilder.Build(query.Table, primaryField, fields, bulkMutates, onConflict)
id, _, err = s.Exec(ctx, statement, args)
)
if err != nil {
return nil, err
}
var (
ids = make([]interface{}, len(bulkMutates))
inc = 1
)
if s.IncrementFunc != nil {
inc = s.IncrementFunc(s)
}
if inc < 0 {
id = id + int64((len(bulkMutates)-1)*inc)
inc *= -1
}
if primaryField != "" {
counter := 0
for i := range ids {
if mut, ok := bulkMutates[i][primaryField]; ok {
ids[i] = mut.Value
id = toInt64(ids[i])
counter = 1
} else {
ids[i] = id + int64(counter*inc)
counter++
}
}
}
return ids, nil
}
// Update updates a record in database.
func (s SQL) Update(ctx context.Context, query rel.Query, primaryField string, mutates map[string]rel.Mutate) (int, error) {
var (
statement, args = s.UpdateBuilder.Build(query.Table, primaryField, mutates, query.WhereQuery)
_, updatedCount, err = s.Exec(ctx, statement, args)
)
return int(updatedCount), err
}
// Delete deletes all results that match the query.
func (s SQL) Delete(ctx context.Context, query rel.Query) (int, error) {
var (
statement, args = s.DeleteBuilder.Build(query.Table, query.WhereQuery)
_, deletedCount, err = s.Exec(ctx, statement, args)
)
return int(deletedCount), err
}
// SchemaApply performs migration to database.
func (s SQL) SchemaApply(ctx context.Context, migration rel.Migration) error {
var (
statement string
)
switch v := migration.(type) {
case rel.Table:
statement = s.TableBuilder.Build(v)
case rel.Index:
statement = s.IndexBuilder.Build(v)
case rel.Raw:
statement = string(v)
}
_, _, err := s.Exec(ctx, statement, nil)
return err
}
// Apply performs migration to database.
//
// Deprecated: Use Schema Apply instead.
func (s SQL) Apply(ctx context.Context, migration rel.Migration) error {
return s.SchemaApply(ctx, migration)
}

@ -0,0 +1,112 @@
package sql
import (
"strings"
"time"
"github.com/go-rel/rel"
)
// DefaultTimeLayout default time layout.
const DefaultTimeLayout = "2006-01-02 15:04:05"
// ColumnMapper function.
func ColumnMapper(column *rel.Column) (string, int, int) {
var (
typ string
m, n int
timeLayout = DefaultTimeLayout
)
switch column.Type {
case rel.ID:
typ = "INT UNSIGNED AUTO_INCREMENT"
case rel.BigID:
typ = "BIGINT UNSIGNED AUTO_INCREMENT"
case rel.Bool:
typ = "BOOL"
case rel.Int:
typ = "INT"
m = column.Limit
case rel.BigInt:
typ = "BIGINT"
m = column.Limit
case rel.Float:
typ = "FLOAT"
m = column.Precision
case rel.Decimal:
typ = "DECIMAL"
m = column.Precision
n = column.Scale
case rel.String:
typ = "VARCHAR"
m = column.Limit
if m == 0 {
m = 255
}
case rel.Text:
typ = "TEXT"
m = column.Limit
case rel.JSON:
typ = "TEXT"
case rel.Date:
typ = "DATE"
timeLayout = "2006-01-02"
case rel.DateTime:
typ = "DATETIME"
case rel.Time:
typ = "TIME"
timeLayout = "15:04:05"
default:
typ = string(column.Type)
}
if t, ok := column.Default.(time.Time); ok {
column.Default = t.Format(timeLayout)
}
return typ, m, n
}
// ExtractString between two string.
func ExtractString(s, left, right string) string {
var (
start = strings.Index(s, left)
end = strings.LastIndex(s, right)
)
if start < 0 || end < 0 || start+len(left) >= end {
return s
}
return s[start+len(left) : end]
}
func toInt64(i interface{}) int64 {
var result int64
switch s := i.(type) {
case int:
result = int64(s)
case int64:
result = s
case int32:
result = int64(s)
case int16:
result = int64(s)
case int8:
result = int64(s)
case uint:
result = int64(s)
case uint64:
result = int64(s)
case uint32:
result = int64(s)
case uint16:
result = int64(s)
case uint8:
result = int64(s)
}
return result
}

@ -0,0 +1,17 @@
language: go
arch:
- amd64
- ppc64le
go:
- 1.8
- 1.9
- tip
# Disable version go:1.8
jobs:
exclude:
- arch: amd64
go: 1.8
- arch: ppc64le
go: 1.8
install: go get -t -d -v ./... && go build -v ./...

@ -0,0 +1,19 @@
Copyright (c) 2015 Serenize UG (haftungsbeschränkt)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,25 @@
# snaker
[![Build Status](https://travis-ci.org/serenize/snaker.svg?branch=master)](https://travis-ci.org/serenize/snaker)
[![GoDoc](https://godoc.org/github.com/serenize/snaker?status.svg)](https://godoc.org/github.com/serenize/snaker)
This is a small utility to convert camel cased strings to snake case and back, except some defined words.
## QBS Usage
To replace the original toSnake and back algorithms for [https://github.com/coocood/qbs](https://github.com/coocood/qbs)
you can easily use snaker:
Import snaker
```go
import (
github.com/coocood/qbs
github.com/serenize/snaker
)
```
Register the snaker methods to qbs
```go
qbs.ColumnNameToFieldName = snaker.SnakeToCamel
qbs.FieldNameToColumnName = snaker.CamelToSnake
```

@ -0,0 +1,150 @@
// Package snaker provides methods to convert CamelCase names to snake_case and back.
// It considers the list of allowed initialsms used by github.com/golang/lint/golint (e.g. ID or HTTP)
package snaker
import (
"strings"
"unicode"
)
// CamelToSnake converts a given string to snake case
func CamelToSnake(s string) string {
var result string
var words []string
var lastPos int
rs := []rune(s)
for i := 0; i < len(rs); i++ {
if i > 0 && unicode.IsUpper(rs[i]) {
if initialism := startsWithInitialism(s[lastPos:]); initialism != "" {
words = append(words, initialism)
i += len(initialism) - 1
lastPos = i
continue
}
words = append(words, s[lastPos:i])
lastPos = i
}
}
// append the last word
if s[lastPos:] != "" {
words = append(words, s[lastPos:])
}
for k, word := range words {
if k > 0 {
result += "_"
}
result += strings.ToLower(word)
}
return result
}
func snakeToCamel(s string, upperCase bool) string {
var result string
words := strings.Split(s, "_")
for i, word := range words {
if exception := snakeToCamelExceptions[word]; len(exception) > 0 {
result += exception
continue
}
if upperCase || i > 0 {
if upper := strings.ToUpper(word); commonInitialisms[upper] {
result += upper
continue
}
}
if (upperCase || i > 0) && len(word) > 0 {
w := []rune(word)
w[0] = unicode.ToUpper(w[0])
result += string(w)
} else {
result += word
}
}
return result
}
// SnakeToCamel returns a string converted from snake case to uppercase
func SnakeToCamel(s string) string {
return snakeToCamel(s, true)
}
// SnakeToCamelLower returns a string converted from snake case to lowercase
func SnakeToCamelLower(s string) string {
return snakeToCamel(s, false)
}
// startsWithInitialism returns the initialism if the given string begins with it
func startsWithInitialism(s string) string {
var initialism string
// the longest initialism is 5 char, the shortest 2
for i := 1; i <= 5; i++ {
if len(s) > i-1 && commonInitialisms[s[:i]] {
initialism = s[:i]
}
}
return initialism
}
// commonInitialisms, taken from
// https://github.com/golang/lint/blob/206c0f020eba0f7fbcfbc467a5eb808037df2ed6/lint.go#L731
var commonInitialisms = map[string]bool{
"ACL": true,
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"ETA": true,
"GPU": true,
"GUID": true,
"HTML": true,
"HTTP": true,
"HTTPS": true,
"ID": true,
"IP": true,
"JSON": true,
"LHS": true,
"OS": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SQL": true,
"SSH": true,
"TCP": true,
"TLS": true,
"TTL": true,
"UDP": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XMPP": true,
"XSRF": true,
"XSS": true,
"OAuth": true,
}
// add exceptions here for things that are not automatically convertable
var snakeToCamelExceptions = map[string]string{
"oauth": "OAuth",
}

@ -1,6 +1,7 @@
package assert
import (
"bytes"
"fmt"
"reflect"
"time"
@ -32,7 +33,8 @@ var (
stringType = reflect.TypeOf("")
timeType = reflect.TypeOf(time.Time{})
timeType = reflect.TypeOf(time.Time{})
bytesType = reflect.TypeOf([]byte{})
)
func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
@ -323,6 +325,26 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64)
}
case reflect.Slice:
{
// We only care about the []byte type.
if !canConvert(obj1Value, bytesType) {
break
}
// []byte can be compared!
bytesObj1, ok := obj1.([]byte)
if !ok {
bytesObj1 = obj1Value.Convert(bytesType).Interface().([]byte)
}
bytesObj2, ok := obj2.([]byte)
if !ok {
bytesObj2 = obj2Value.Convert(bytesType).Interface().([]byte)
}
return CompareType(bytes.Compare(bytesObj1, bytesObj2)), true
}
}
return compareEqual, false

@ -736,6 +736,16 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim
return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...)
}
// WithinRangef asserts that a time is within a time range (inclusive).
//
// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted")
func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
return WithinRange(t, actual, start, end, append([]interface{}{msg}, args...)...)
}
// YAMLEqf asserts that two YAML strings are equivalent.
func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool {
if h, ok := t.(tHelper); ok {

@ -1461,6 +1461,26 @@ func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta
return WithinDurationf(a.t, expected, actual, delta, msg, args...)
}
// WithinRange asserts that a time is within a time range (inclusive).
//
// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))
func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return WithinRange(a.t, actual, start, end, msgAndArgs...)
}
// WithinRangef asserts that a time is within a time range (inclusive).
//
// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted")
func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
return WithinRangef(a.t, actual, start, end, msg, args...)
}
// YAMLEq asserts that two YAML strings are equivalent.
func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool {
if h, ok := a.t.(tHelper); ok {

@ -8,6 +8,7 @@ import (
"fmt"
"math"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
@ -144,7 +145,8 @@ func CallerInfo() []string {
if len(parts) > 1 {
dir := parts[len(parts)-2]
if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" {
callers = append(callers, fmt.Sprintf("%s:%d", file, line))
path, _ := filepath.Abs(file)
callers = append(callers, fmt.Sprintf("%s:%d", path, line))
}
}
@ -563,16 +565,17 @@ func isEmpty(object interface{}) bool {
switch objValue.Kind() {
// collection types are empty when they have no element
case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
case reflect.Chan, reflect.Map, reflect.Slice:
return objValue.Len() == 0
// pointers are empty if nil or if the value they point to is empty
// pointers are empty if nil or if the value they point to is empty
case reflect.Ptr:
if objValue.IsNil() {
return true
}
deref := objValue.Elem().Interface()
return isEmpty(deref)
// for all other types, compare against the zero value
// for all other types, compare against the zero value
// array types are empty when they match their zero-initialized state
default:
zero := reflect.Zero(objValue.Type())
return reflect.DeepEqual(object, zero.Interface())
@ -815,7 +818,6 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
return true // we consider nil to be equal to the nil set
}
subsetValue := reflect.ValueOf(subset)
defer func() {
if e := recover(); e != nil {
ok = false
@ -825,14 +827,32 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok
listKind := reflect.TypeOf(list).Kind()
subsetKind := reflect.TypeOf(subset).Kind()
if listKind != reflect.Array && listKind != reflect.Slice {
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
}
if subsetKind != reflect.Array && subsetKind != reflect.Slice {
if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
}
subsetValue := reflect.ValueOf(subset)
if subsetKind == reflect.Map && listKind == reflect.Map {
listValue := reflect.ValueOf(list)
subsetKeys := subsetValue.MapKeys()
for i := 0; i < len(subsetKeys); i++ {
subsetKey := subsetKeys[i]
subsetElement := subsetValue.MapIndex(subsetKey).Interface()
listElement := listValue.MapIndex(subsetKey).Interface()
if !ObjectsAreEqual(subsetElement, listElement) {
return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, subsetElement), msgAndArgs...)
}
}
return true
}
for i := 0; i < subsetValue.Len(); i++ {
element := subsetValue.Index(i).Interface()
ok, found := containsElement(list, element)
@ -859,7 +879,6 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...)
}
subsetValue := reflect.ValueOf(subset)
defer func() {
if e := recover(); e != nil {
ok = false
@ -869,14 +888,32 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{})
listKind := reflect.TypeOf(list).Kind()
subsetKind := reflect.TypeOf(subset).Kind()
if listKind != reflect.Array && listKind != reflect.Slice {
if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
}
if subsetKind != reflect.Array && subsetKind != reflect.Slice {
if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map {
return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
}
subsetValue := reflect.ValueOf(subset)
if subsetKind == reflect.Map && listKind == reflect.Map {
listValue := reflect.ValueOf(list)
subsetKeys := subsetValue.MapKeys()
for i := 0; i < len(subsetKeys); i++ {
subsetKey := subsetKeys[i]
subsetElement := subsetValue.MapIndex(subsetKey).Interface()
listElement := listValue.MapIndex(subsetKey).Interface()
if !ObjectsAreEqual(subsetElement, listElement) {
return true
}
}
return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...)
}
for i := 0; i < subsetValue.Len(); i++ {
element := subsetValue.Index(i).Interface()
ok, found := containsElement(list, element)
@ -1109,6 +1146,27 @@ func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration,
return true
}
// WithinRange asserts that a time is within a time range (inclusive).
//
// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))
func WithinRange(t TestingT, actual, start, end time.Time, msgAndArgs ...interface{}) bool {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if end.Before(start) {
return Fail(t, "Start should be before end", msgAndArgs...)
}
if actual.Before(start) {
return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is before the range", actual, start, end), msgAndArgs...)
} else if actual.After(end) {
return Fail(t, fmt.Sprintf("Time %v expected to be in time range %v to %v, but is after the range", actual, start, end), msgAndArgs...)
}
return true
}
func toFloat(x interface{}) (float64, bool) {
var xf float64
xok := true

@ -1864,6 +1864,32 @@ func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta tim
t.FailNow()
}
// WithinRange asserts that a time is within a time range (inclusive).
//
// assert.WithinRange(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))
func WithinRange(t TestingT, actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if assert.WithinRange(t, actual, start, end, msgAndArgs...) {
return
}
t.FailNow()
}
// WithinRangef asserts that a time is within a time range (inclusive).
//
// assert.WithinRangef(t, time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted")
func WithinRangef(t TestingT, actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) {
if h, ok := t.(tHelper); ok {
h.Helper()
}
if assert.WithinRangef(t, actual, start, end, msg, args...) {
return
}
t.FailNow()
}
// YAMLEq asserts that two YAML strings are equivalent.
func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) {
if h, ok := t.(tHelper); ok {

@ -1462,6 +1462,26 @@ func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta
WithinDurationf(a.t, expected, actual, delta, msg, args...)
}
// WithinRange asserts that a time is within a time range (inclusive).
//
// a.WithinRange(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second))
func (a *Assertions) WithinRange(actual time.Time, start time.Time, end time.Time, msgAndArgs ...interface{}) {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
WithinRange(a.t, actual, start, end, msgAndArgs...)
}
// WithinRangef asserts that a time is within a time range (inclusive).
//
// a.WithinRangef(time.Now(), time.Now().Add(-time.Second), time.Now().Add(time.Second), "error message %s", "formatted")
func (a *Assertions) WithinRangef(actual time.Time, start time.Time, end time.Time, msg string, args ...interface{}) {
if h, ok := a.t.(tHelper); ok {
h.Helper()
}
WithinRangef(a.t, actual, start, end, msg, args...)
}
// YAMLEq asserts that two YAML strings are equivalent.
func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) {
if h, ok := a.t.(tHelper); ok {

15
vendor/modules.txt vendored

@ -189,6 +189,16 @@ github.com/go-redis/redis/v8/internal/pool
github.com/go-redis/redis/v8/internal/proto
github.com/go-redis/redis/v8/internal/rand
github.com/go-redis/redis/v8/internal/util
# github.com/go-rel/mysql v0.8.0
## explicit; go 1.17
github.com/go-rel/mysql
# github.com/go-rel/rel v0.38.0
## explicit; go 1.15
github.com/go-rel/rel
# github.com/go-rel/sql v0.11.0
## explicit; go 1.16
github.com/go-rel/sql
github.com/go-rel/sql/builder
# github.com/go-sql-driver/mysql v1.6.0
## explicit; go 1.10
github.com/go-sql-driver/mysql
@ -420,6 +430,9 @@ github.com/saracen/go7z/headers
# github.com/saracen/solidblock v0.0.0-20190426153529-45df20abab6f
## explicit
github.com/saracen/solidblock
# github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e
## explicit
github.com/serenize/snaker
# github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18
## explicit
github.com/shiena/ansicolor
@ -429,7 +442,7 @@ github.com/shopspring/decimal
# github.com/sirupsen/logrus v1.8.1
## explicit; go 1.13
github.com/sirupsen/logrus
# github.com/stretchr/testify v1.7.2
# github.com/stretchr/testify v1.8.0
## explicit; go 1.13
github.com/stretchr/testify/assert
github.com/stretchr/testify/require

Loading…
Cancel
Save