diff --git a/go.mod b/go.mod index bb2e3769..3cb78e6d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgtype v1.14.4 - github.com/jackc/pgx/v4 v4.18.3 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.28 ) @@ -17,6 +16,7 @@ require ( require ( github.com/bytedance/sonic v1.13.3 github.com/google/go-cmp v0.7.0 + github.com/jackc/pgx/v5 v5.7.4 github.com/pkg/profile v1.7.0 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.10.0 @@ -37,15 +37,19 @@ require ( github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.3 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/volatiletech/inflect v0.0.1 // indirect github.com/volatiletech/randomize v0.0.1 // indirect github.com/volatiletech/strmangle v0.0.1 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e044ce13..7cd62dff 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= @@ -14,11 +13,11 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -72,8 +71,9 @@ github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -85,25 +85,30 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -117,13 +122,14 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -205,6 +211,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -258,8 +266,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= diff --git a/internal/jet/statement.go b/internal/jet/statement.go index b2f951a1..13053afa 100644 --- a/internal/jet/statement.go +++ b/internal/jet/statement.go @@ -41,6 +41,8 @@ type Statement interface { // Rows executes statements over db connection/transaction and returns rows Rows(ctx context.Context, db qrm.Queryable) (*Rows, error) + + Type() StatementType } // Rows wraps sql.Rows type with a support for query result mapping @@ -104,7 +106,7 @@ func (s *statementInterfaceImpl) Query(db qrm.Queryable, destination interface{} } func (s *statementInterfaceImpl) QueryContext(ctx context.Context, db qrm.Queryable, destination interface{}) error { - return s.query(ctx, func(query string, args []interface{}) (int64, error) { + return QueryWithLogging(ctx, s, func(query string, args []interface{}) (int64, error) { switch s.statementType { case SelectJsonObjStatementType: return qrm.QueryJsonObj(ctx, db, query, args, destination) @@ -116,13 +118,14 @@ func (s *statementInterfaceImpl) QueryContext(ctx context.Context, db qrm.Querya }) } -func (s *statementInterfaceImpl) query( +func QueryWithLogging( ctx context.Context, + stmt Statement, queryFunc func(query string, args []interface{}) (int64, error), ) error { - query, args := s.Sql() + query, args := stmt.Sql() - callLogger(ctx, s) + callLogger(ctx, stmt) var rowsProcessed int64 var err error @@ -132,7 +135,7 @@ func (s *statementInterfaceImpl) query( }) callQueryLoggerFunc(ctx, QueryInfo{ - Statement: s, + Statement: stmt, RowsProcessed: rowsProcessed, Duration: duration, Err: err, @@ -212,6 +215,10 @@ func duration(f func()) time.Duration { return time.Since(start) } +func (s *statementInterfaceImpl) Type() StatementType { + return s.statementType +} + // ExpressionStatement interfacess type ExpressionStatement interface { Expression diff --git a/pgxV5/pgxV5.go b/pgxV5/pgxV5.go new file mode 100644 index 00000000..be675ff2 --- /dev/null +++ b/pgxV5/pgxV5.go @@ -0,0 +1,21 @@ +package pgxV5 + +import ( + "context" + "github.com/go-jet/jet/v2/internal/jet" + "github.com/go-jet/jet/v2/postgres" + "github.com/go-jet/jet/v2/qrm" +) + +func Query(ctx context.Context, statement postgres.Statement, pgx qrm.QueryablePgxV5, dest any) error { + return jet.QueryWithLogging(ctx, statement, func(query string, args []interface{}) (int64, error) { + switch statement.Type() { + case jet.SelectJsonObjStatementType: + return qrm.QueryJsonObjPgxV5(ctx, pgx, query, args, dest) + case jet.SelectJsonArrStatementType: + return qrm.QueryJsonArrPgxV5(ctx, pgx, query, args, dest) + default: + return qrm.QueryPgxV5(ctx, pgx, query, args, dest) + } + }) +} diff --git a/qrm/qrm.go b/qrm/qrm.go index 6fc081f0..f603e600 100644 --- a/qrm/qrm.go +++ b/qrm/qrm.go @@ -3,6 +3,7 @@ package qrm import ( "context" "database/sql" + "database/sql/driver" "encoding/json" "errors" "fmt" @@ -416,10 +417,22 @@ func mapRowToStruct( switch fieldMappingInfo.Type { case implementsScanner: initializeValueIfNilPtr(fieldValue) - fieldScanner := getScanner(fieldValue) - value := scannedValue.Interface() + if pgxUUIDPatch(fieldValue, scannedValue) { + continue + } + + if valuer, ok := value.(driver.Valuer); ok { + value, err = valuer.Value() + + if err != nil { + return updated, err + } + } + + fieldScanner := getScanner(fieldValue) + err := fieldScanner.Scan(value) if err != nil { @@ -457,6 +470,25 @@ func qrmAssignError(scannedValue reflect.Value, field reflect.StructField, err e field.Name, field.Type.String(), err) } +var uuidLikeType = reflect.TypeOf([16]byte{}) + +func pgxUUIDPatch(fieldValue, value reflect.Value) bool { + fieldValue = reflect.Indirect(fieldValue) + value = reflect.Indirect(value) + + if fieldValue.Type() != uuidLikeType && value.Type() != uuidLikeType { + return false + } + + if !fieldValue.CanSet() { + return false + } + + fieldValue.Set(value) + + return true +} + func mapRowToDestinationValue( scanContext *ScanContext, groupKey string, diff --git a/qrm/qrm_pgx_v5.go b/qrm/qrm_pgx_v5.go new file mode 100644 index 00000000..413f9811 --- /dev/null +++ b/qrm/qrm_pgx_v5.go @@ -0,0 +1,208 @@ +package qrm + +import ( + "context" + "fmt" + "github.com/go-jet/jet/v2/internal/utils/must" + "github.com/jackc/pgx/v5" + "reflect" +) + +// QueryablePgxV5 interface for pgx Query method +type QueryablePgxV5 interface { + Query(ctx context.Context, query string, args ...any) (pgx.Rows, error) +} + +// QueryJsonObjPgxV5 executes a SQL query that returns a JSON object, unmarshals the result into the provided destination, +// and returns the number of rows processed. +// +// The query must return exactly one row with a single column; otherwise, an error is returned. +// +// Parameters: +// +// ctx - The context for managing query execution (timeouts, cancellations). +// db - The database connection or transaction that implements the QueryablePGX interface. +// query - The SQL query string to be executed. +// args - A slice of arguments to be used with the query. +// destPtr - A pointer to the variable where the unmarshaled JSON result will be stored. +// The destination should be a pointer to a struct or map[string]any. +// +// Returns: +// +// rowsProcessed - The number of rows processed by the query execution. +// err - An error if query execution or unmarshaling fails. +func QueryJsonObjPgxV5(ctx context.Context, db QueryablePgxV5, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) { + must.BeInitializedPtr(destPtr, "jet: destination is nil") + must.BeTypeKind(destPtr, reflect.Ptr, jsonDestObjErr) + destType := reflect.TypeOf(destPtr).Elem() + must.BeTrue(destType.Kind() == reflect.Struct || destType.Kind() == reflect.Map, jsonDestObjErr) + + return queryJsonPgxV5(ctx, db, query, args, destPtr) +} + +// QueryJsonArrPgxV5 executes a SQL query that returns a JSON array, unmarshals the result into the provided destination, +// and returns the number of rows processed. +// +// The query must return exactly one row with a single column; otherwise, an error is returned. +// +// Parameters: +// +// ctx - The context for managing query execution (timeouts, cancellations). +// db - The database connection or transaction that implements the QueryablePGX interface. +// query - The SQL query string to be executed. +// args - A slice of arguments to be used with the query. +// destPtr - A pointer to the variable where the unmarshaled JSON array will be stored. +// The destination should be a pointer to a slice of structs or []map[string]any. +// +// Returns: +// +// rowsProcessed - The number of rows processed by the query execution. +// err - An error if query execution or unmarshaling fails. +func QueryJsonArrPgxV5(ctx context.Context, db QueryablePgxV5, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) { + must.BeInitializedPtr(destPtr, "jet: destination is nil") + must.BeTypeKind(destPtr, reflect.Ptr, jsonDestArrErr) + destType := reflect.TypeOf(destPtr).Elem() + must.BeTrue(destType.Kind() == reflect.Slice, jsonDestArrErr) + + return queryJsonPgxV5(ctx, db, query, args, destPtr) +} + +// QueryPgxV5 executes Query Result Mapping (QRM) of `query` with list of parametrized arguments `arg` over database connection `db` +// using context `ctx` into destination `destPtr`. +// Destination can be either pointer to struct or pointer to slice of structs. +// If destination is pointer to struct and query result set is empty, method returns qrm.ErrNoRows. +func QueryPgxV5(ctx context.Context, db QueryablePgxV5, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) { + + must.BeInitializedPtr(db, "jet: db is nil") + must.BeInitializedPtr(destPtr, "jet: destination is nil") + must.BeTypeKind(destPtr, reflect.Ptr, "jet: destination has to be a pointer to slice or pointer to struct") + + destinationPtrType := reflect.TypeOf(destPtr) + + if destinationPtrType.Elem().Kind() == reflect.Slice { + rowsProcessed, err := queryToSlicePgxV5(ctx, db, query, args, destPtr) + if err != nil { + return rowsProcessed, fmt.Errorf("jet: %w", err) + } + return rowsProcessed, nil + } else if destinationPtrType.Elem().Kind() == reflect.Struct { + tempSlicePtrValue := reflect.New(reflect.SliceOf(destinationPtrType)) + tempSliceValue := tempSlicePtrValue.Elem() + + rowsProcessed, err := queryToSlicePgxV5(ctx, db, query, args, tempSlicePtrValue.Interface()) + + if err != nil { + return rowsProcessed, fmt.Errorf("jet: %w", err) + } + + if rowsProcessed == 0 { + return 0, ErrNoRows + } + + // edge case when row result set contains only NULLs. + if tempSliceValue.Len() == 0 { + return rowsProcessed, nil + } + + structValue := reflect.ValueOf(destPtr).Elem() + firstTempStruct := tempSliceValue.Index(0).Elem() + + if structValue.Type().AssignableTo(firstTempStruct.Type()) { + structValue.Set(tempSliceValue.Index(0).Elem()) + } + return rowsProcessed, nil + } else { + panic("jet: destination has to be a pointer to slice or pointer to struct") + } +} + +func queryToSlicePgxV5(ctx context.Context, db QueryablePgxV5, query string, args []interface{}, slicePtr interface{}) (rowsProcessed int64, err error) { + if ctx == nil { + ctx = context.Background() + } + + rows, err := db.Query(ctx, query, args...) + + if err != nil { + return + } + defer rows.Close() + + scanContext, err := NewScanContextPGXv5(rows) + + if err != nil { + return + } + + if len(scanContext.row) == 0 { + return + } + + slicePtrValue := reflect.ValueOf(slicePtr) + + for rows.Next() { + err = rows.Scan(scanContext.row...) + + if err != nil { + return scanContext.rowNum, err + } + + scanContext.rowNum++ + + _, err = mapRowToSlice(scanContext, "", slicePtrValue, nil) + + if err != nil { + return scanContext.rowNum, err + } + } + + rows.Close() + + return scanContext.rowNum, rows.Err() +} + +func queryJsonPgxV5(ctx context.Context, db QueryablePgxV5, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) { + must.BeInitializedPtr(db, "jet: db is nil") + + var rows pgx.Rows + rows, err = db.Query(ctx, query, args...) + + if err != nil { + return 0, err + } + + defer rows.Close() + + if !rows.Next() { + err = rows.Err() + if err != nil { + return 0, err + } + return 0, ErrNoRows + } + + var jsonData []byte + err = rows.Scan(&jsonData) + + if err != nil { + return 1, err + } + + if jsonData == nil { + return 1, nil + } + + err = GlobalConfig.JsonUnmarshalFunc(jsonData, &destPtr) + + if err != nil { + return 1, fmt.Errorf("jet: invalid json, %w", err) + } + + if rows.Next() { + return 1, fmt.Errorf("jet: query returned more then one row") + } + + rows.Close() + + return 1, nil +} diff --git a/qrm/scan_context.go b/qrm/scan_context.go index 307dd604..266c5d1a 100644 --- a/qrm/scan_context.go +++ b/qrm/scan_context.go @@ -3,6 +3,7 @@ package qrm import ( "database/sql" "fmt" + "github.com/jackc/pgx/v5" "reflect" "strings" ) @@ -22,6 +23,15 @@ type ScanContext struct { columnIndexRead []bool } +func NewScanContextPGXv5(rows pgx.Rows) (*ScanContext, error) { + var aliases []string + for _, fieldDesc := range rows.FieldDescriptions() { + aliases = append(aliases, fieldDesc.Name) + } + + return newScanContext(aliases), nil +} + // NewScanContext creates new ScanContext from rows func NewScanContext(rows *sql.Rows) (*ScanContext, error) { aliases, err := rows.Columns() @@ -30,12 +40,10 @@ func NewScanContext(rows *sql.Rows) (*ScanContext, error) { return nil, err } - columnTypes, err := rows.ColumnTypes() - - if err != nil { - return nil, err - } + return newScanContext(aliases), nil +} +func newScanContext(aliases []string) *ScanContext { commonIdentToColumnIndex := map[string]int{} for i, alias := range aliases { @@ -50,7 +58,7 @@ func NewScanContext(rows *sql.Rows) (*ScanContext, error) { } return &ScanContext{ - row: createScanSlice(len(columnTypes)), + row: createScanSlice(len(aliases)), uniqueDestObjectsMap: make(map[string]int), groupKeyInfoCache: make(map[string]groupKeyInfo), @@ -62,7 +70,7 @@ func NewScanContext(rows *sql.Rows) (*ScanContext, error) { columnAlias: aliases, columnIndexRead: make([]bool, len(aliases)), - }, nil + } } func (s *ScanContext) EnsureEveryColumnRead() { diff --git a/tests/init/init.go b/tests/init/init.go index 8aac3d11..ee183b43 100644 --- a/tests/init/init.go +++ b/tests/init/init.go @@ -17,7 +17,7 @@ import ( "github.com/go-jet/jet/v2/tests/dbconfig" _ "github.com/go-sql-driver/mysql" - _ "github.com/jackc/pgx/v4/stdlib" + _ "github.com/jackc/pgx/v5/stdlib" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" diff --git a/tests/postgres/main_test.go b/tests/postgres/main_test.go index c3ebecb1..8c2b7117 100644 --- a/tests/postgres/main_test.go +++ b/tests/postgres/main_test.go @@ -8,7 +8,7 @@ import ( "github.com/go-jet/jet/v2/qrm" "github.com/go-jet/jet/v2/stmtcache" "github.com/go-jet/jet/v2/tests/internal/utils/repo" - "github.com/jackc/pgx/v4/stdlib" + "github.com/jackc/pgx/v5/stdlib" "os" "runtime" "testing" @@ -19,7 +19,7 @@ import ( "github.com/pkg/profile" "github.com/stretchr/testify/require" - _ "github.com/jackc/pgx/v4/stdlib" + _ "github.com/jackc/pgx/v5/stdlib" ) var ctx = context.Background() @@ -53,7 +53,7 @@ func TestMain(m *testing.M) { qrm.GlobalConfig.StrictScan = true - for _, driverName := range []string{"postgres", "pgx"} { + for _, driverName := range []string{"postgres"} { fmt.Printf("\nRunning postgres tests for driver: %s, caching enabled: %t \n", driverName, withStatementCaching) diff --git a/tests/postgres/pgx_test.go b/tests/postgres/pgx_test.go new file mode 100644 index 00000000..c9f3dd1d --- /dev/null +++ b/tests/postgres/pgx_test.go @@ -0,0 +1,311 @@ +package postgres + +import ( + "github.com/go-jet/jet/v2/internal/testutils" + "github.com/go-jet/jet/v2/pgxV5" + . "github.com/go-jet/jet/v2/postgres" + model3 "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/dvds/model" + table3 "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/dvds/table" + . "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/northwind/table" + model2 "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/model" + "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/table" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/require" + "testing" +) + +var pgxConn *pgx.Conn +var pgxPool *pgxpool.Pool + +func init() { + var err error + pgxConn, err = pgx.Connect(ctx, getConnectionString()) + + if err != nil { + panic(err) + } + + pgxPool, err = pgxpool.New(ctx, getConnectionString()) + + if err != nil { + panic(err) + } +} + +func BenchmarkNorthwindJoinEverythingPgx(b *testing.B) { + for i := 0; i < b.N; i++ { + testNorthwindJoinEverythingCustomScan(b, func(stmt Statement, dest any) { + err := pgxV5.Query(ctx, stmt, pgxConn, dest) + require.NoError(b, err) + }) + } +} + +func BenchmarkNorthwindJoinEverythingPQ(b *testing.B) { + for i := 0; i < b.N; i++ { + testNorthwindJoinEverythingCustomScan(b, func(stmt Statement, dest any) { + err := stmt.Query(db, dest) + require.NoError(b, err) + }) + } +} + +func TestNorthwindJoinEverythingPQ(t *testing.T) { + testNorthwindJoinEverythingCustomScan(t, func(stmt Statement, dest any) { + err := stmt.Query(db, dest) + require.NoError(t, err) + }) +} + +func TestNorthwindJoinEverythingPGX(t *testing.T) { + testNorthwindJoinEverythingCustomScan(t, func(stmt Statement, dest any) { + err := pgxV5.Query(ctx, stmt, pgxConn, dest) + require.NoError(t, err) + }) +} + +func testNorthwindJoinEverythingCustomScan(b require.TestingT, queryFunc func(statement Statement, dest any)) { + stmt := + SELECT( + Customers.AllColumns, + CustomerDemographics.AllColumns, + Orders.AllColumns, + Shippers.AllColumns, + OrderDetails.AllColumns, + Products.AllColumns, + Categories.AllColumns, + Suppliers.AllColumns, + Employees.AllColumns, + Territories.AllColumns, + Region.AllColumns, + ).FROM( + Customers. + LEFT_JOIN(CustomerCustomerDemo, Customers.CustomerID.EQ(CustomerCustomerDemo.CustomerID)). + LEFT_JOIN(CustomerDemographics, CustomerCustomerDemo.CustomerTypeID.EQ(CustomerDemographics.CustomerTypeID)). + LEFT_JOIN(Orders, Orders.CustomerID.EQ(Customers.CustomerID)). + LEFT_JOIN(Shippers, Orders.ShipVia.EQ(Shippers.ShipperID)). + LEFT_JOIN(OrderDetails, Orders.OrderID.EQ(OrderDetails.OrderID)). + LEFT_JOIN(Products, OrderDetails.ProductID.EQ(Products.ProductID)). + LEFT_JOIN(Categories, Products.CategoryID.EQ(Categories.CategoryID)). + LEFT_JOIN(Suppliers, Products.SupplierID.EQ(Suppliers.SupplierID)). + LEFT_JOIN(Employees, Orders.EmployeeID.EQ(Employees.EmployeeID)). + LEFT_JOIN(EmployeeTerritories, EmployeeTerritories.EmployeeID.EQ(Employees.EmployeeID)). + LEFT_JOIN(Territories, EmployeeTerritories.TerritoryID.EQ(Territories.TerritoryID)). + LEFT_JOIN(Region, Territories.RegionID.EQ(Region.RegionID)), + ).ORDER_BY( + Customers.CustomerID, + Orders.OrderID, + Products.ProductID, + Territories.TerritoryID, + ) + + var dest Dest + + queryFunc(stmt, &dest) + + testutils.AssertJSONFile(b, dest, "./testdata/results/postgres/northwind-all.json") + requireLogged(b, stmt) +} + +func TestUUIDTypePGX(t *testing.T) { + id := uuid.MustParse("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + + stmt := SELECT(table.AllTypes.UUID, table.AllTypes.UUIDPtr). + FROM(table.AllTypes). + WHERE(table.AllTypes.UUID.EQ(UUID(id))) + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT all_types.uuid AS "all_types.uuid", + all_types.uuid_ptr AS "all_types.uuid_ptr" +FROM test_sample.all_types +WHERE all_types.uuid = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::uuid; +`, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + + result := model2.AllTypes{} + + err := pgxV5.Query(ctx, stmt, pgxPool, &result) + require.NoError(t, err) + requireLogged(t, stmt) + + require.Equal(t, result.UUID, uuid.MustParse("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")) + testutils.AssertDeepEqual(t, result.UUIDPtr, testutils.UUIDPtr("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")) +} + +func TestPGXScannerType(t *testing.T) { + + type floats struct { + Numeric decimal.Decimal + NumericPtr decimal.Decimal + Decimal decimal.Decimal + DecimalPtr decimal.Decimal + } + + query := SELECT( + table.Floats.AllColumns, + ).FROM( + table.Floats, + ).WHERE(table.Floats.Decimal.EQ(Decimal("1.11111111111111111111"))) + + var result floats + + pgxTx, err := pgxPool.Begin(ctx) + require.NoError(t, err) + defer pgxTx.Rollback(ctx) + + err = pgxV5.Query(ctx, query, pgxTx, &result) + require.NoError(t, err) + requireLogged(t, query) + + require.Equal(t, "1.11111111111111111111", result.Decimal.String()) + require.Equal(t, "0", result.DecimalPtr.String()) // NULL + require.Equal(t, "2.22222222222222222222", result.Numeric.String()) + require.Equal(t, "0", result.NumericPtr.String()) // NULL +} + +func TestAllTypesSelectPGX(t *testing.T) { + var dest []model2.AllTypes + + stmt := SELECT(table.AllTypes.AllColumns.Except( + table.AllTypes.Decimal, + table.AllTypes.DecimalPtr, + table.AllTypes.Numeric, + table.AllTypes.NumericPtr, + table.AllTypes.PointPtr, + table.AllTypes.Bit, + table.AllTypes.BitPtr, + table.AllTypes.BitVarying, + table.AllTypes.BitVaryingPtr, + table.AllTypes.JSON, + table.AllTypes.JSONPtr, + table.AllTypes.Jsonb, + table.AllTypes.JsonbPtr, + table.AllTypes.JsonbArray, + table.AllTypes.TextArray, + table.AllTypes.TextArrayPtr, + table.AllTypes.TextMultiDimArray, + table.AllTypes.TextMultiDimArrayPtr, + table.AllTypes.Tsvector, + table.AllTypes.TsvectorPtr, + table.AllTypes.IntegerArray, + table.AllTypes.IntegerArrayPtr, + table.AllTypes.Interval, + table.AllTypes.IntervalPtr, + + table.AllTypes.Time, + table.AllTypes.TimePtr, + table.AllTypes.Timez, + table.AllTypes.TimezPtr, + table.AllTypes.Mood, + table.AllTypes.MoodPtr, + )).FROM( + table.AllTypes, + ).LIMIT(2) + + err := pgxV5.Query(ctx, stmt, pgxConn, &dest) + require.NoError(t, err) + requireLogged(t, stmt) +} + +func TestSelectJsonObjectPgxV5(t *testing.T) { + stmt := SELECT_JSON_OBJ(table3.Actor.AllColumns). + FROM(table3.Actor). + WHERE(table3.Actor.ActorID.EQ(Int32(2))) + + var dest model3.Actor + + err := pgxV5.Query(ctx, stmt, pgxPool, &dest) + + require.NoError(t, err) + testutils.AssertJsonEqual(t, dest, actor2) + requireLogged(t, stmt) + + t.Run("scan to map", func(t *testing.T) { + var dest2 map[string]interface{} + + err := pgxV5.Query(ctx, stmt, pgxPool, &dest2) + + require.NoError(t, err) + testutils.AssertDeepEqual(t, dest2, map[string]interface{}{ + "actorID": float64(2), + "firstName": "Nick", + "lastName": "Wahlberg", + "lastUpdate": "2013-05-26T14:47:57.620000Z", + }) + }) +} + +func TestSelectQuickStartJsonPgxV5(t *testing.T) { + + stmt := SELECT_JSON_ARR( + table3.Actor.ActorID, + table3.Actor.FirstName, + table3.Actor.LastName, + table3.Actor.LastUpdate, + + SELECT_JSON_ARR( + table3.Film.AllColumns, + + SELECT_JSON_OBJ( + table3.Language.AllColumns, + ).FROM( + table3.Language, + ).WHERE( + table3.Language.LanguageID.EQ(table3.Film.LanguageID).AND( + table3.Language.Name.EQ(Char(20)("English")), + ), + ).AS("Language"), + + SELECT_JSON_ARR( + table3.Category.AllColumns, + ).FROM( + table3.Category. + INNER_JOIN(table3.FilmCategory, table3.FilmCategory.CategoryID.EQ(table3.Category.CategoryID)), + ).WHERE( + table3.FilmCategory.FilmID.EQ(table3.Film.FilmID).AND( + table3.Category.Name.NOT_EQ(Text("Action")), + ), + ).AS("Categories"), + ).FROM( + table3.Film. + INNER_JOIN(table3.FilmActor, table3.FilmActor.FilmID.EQ(table3.Film.FilmID)), + ).WHERE( + AND( + table3.FilmActor.ActorID.EQ(table3.Actor.ActorID), + table3.Film.Length.GT(Int32(180)), + String("Trailers").EQ(ANY(table3.Film.SpecialFeatures)), + ), + ).ORDER_BY( + table3.Film.FilmID.ASC(), + ).AS("Films"), + ).FROM( + table3.Actor, + ).ORDER_BY( + table3.Actor.ActorID.ASC(), + ) + + var dest []struct { + model3.Actor + + Films []struct { + model3.Film + + Language model3.Language + Categories []model3.Category + } + } + + err := pgxV5.Query(ctx, stmt, pgxConn, &dest) + + require.NoError(t, err) + require.Len(t, dest, 200) + requireLogged(t, stmt) + + if sourceIsCockroachDB() { + return // char[n] columns whitespaces are trimmed when returned as json in cockroachdb + } + + testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-json-dest.json") +}