From aae45814f2baea0160e121ee4d93137256152e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Sun, 24 Aug 2025 23:24:00 +0100 Subject: [PATCH] refactor: swap strings with an SQL builder Adds doug-martin/goqu as dependency. Make tests less fragile by focusing only on relevant SQL query parts. --- cmd/api-server/main.go | 4 +- go.mod | 1 + go.sum | 26 +++++ internal/dal/impl.go | 197 +++++++++++++++++++++++--------------- internal/dal/impl_test.go | 60 ++++-------- 5 files changed, 172 insertions(+), 116 deletions(-) diff --git a/cmd/api-server/main.go b/cmd/api-server/main.go index a6d49c7..c0ff7dc 100644 --- a/cmd/api-server/main.go +++ b/cmd/api-server/main.go @@ -9,6 +9,8 @@ import ( "git.rosemyrtle.work/personal-finance/server/internal/api" "git.rosemyrtle.work/personal-finance/server/internal/dal" + "github.com/doug-martin/goqu/v9" + _ "github.com/doug-martin/goqu/v9/dialect/postgres" _ "github.com/jackc/pgx/v5/stdlib" "github.com/joho/godotenv" "github.com/labstack/echo/v4" @@ -39,7 +41,7 @@ func main() { } // 2. Data Access Layer - dal := dal.DalImpl{Db: db} + dal := dal.DalImpl{Db: db, Dialect: goqu.Dialect("postgres")} // 3. HTTP server e := echo.New() diff --git a/go.mod b/go.mod index 7944e08..94e0663 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.1 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/deepmap/oapi-codegen/v2 v2.1.0 + github.com/doug-martin/goqu/v9 v9.0.0 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.5.5 github.com/joho/godotenv v1.5.1 diff --git a/go.sum b/go.sum index 91b82ed..a94c22d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= @@ -9,16 +10,20 @@ 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= github.com/deepmap/oapi-codegen/v2 v2.1.0 h1:I/NMVhJCtuvL9x+S2QzZKpSjGi33oDZwPRdemvOZWyQ= github.com/deepmap/oapi-codegen/v2 v2.1.0/go.mod h1:R1wL226vc5VmCNJUvMyYr3hJMm5reyv25j952zAVXZ8= +github.com/doug-martin/goqu/v9 v9.0.0 h1:de8P6AsHnCPZsdKQl8lrqI8XNxU3K//jrDrI2ZeYcDs= +github.com/doug-martin/goqu/v9 v9.0.0/go.mod h1:GPZzBWikLe6aaCT2uBT5X3MTseGBYevzWiVjMl2wIWk= github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -47,6 +52,8 @@ github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+k github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -54,6 +61,7 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/oapi-codegen/echo-middleware v1.0.1 h1:edYGScq1phCcuDoz9AqA9eHX+tEI1LNL5PL1lkkQh1k= @@ -70,7 +78,10 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -82,29 +93,44 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/dal/impl.go b/internal/dal/impl.go index cf0ff16..62e298c 100644 --- a/internal/dal/impl.go +++ b/internal/dal/impl.go @@ -5,17 +5,28 @@ import ( "errors" "fmt" "log" - "strings" "time" "git.rosemyrtle.work/personal-finance/server/internal/entity" + "github.com/doug-martin/goqu/v9" "github.com/shopspring/decimal" ) type DalImpl struct { - Db *sql.DB + Db *sql.DB + Dialect goqu.DialectWrapper } +// Table aliases +var ( + schema = goqu.S("pfbudget") + t = schema.Table("transactions") + tc = schema.Table("transactions_categorized") + b = schema.Table("banks") + bn = schema.Table("banks_nordigen") + c = schema.Table("categories") +) + func (dal *DalImpl) Transaction(transactionId int64) (*entity.Transaction, error) { log.Printf("DAL::Transaction(%v)", transactionId) @@ -23,15 +34,23 @@ func (dal *DalImpl) Transaction(transactionId int64) (*entity.Transaction, error log.Panic("database not available") } - stmts := []string{ - "SELECT t.id, t.date, t.description, t.amount, tc.name", - "FROM pfbudget.transactions t", - "LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id", - "WHERE t.id = $1", - } - stmt := strings.Join(stmts, "\n") + "\n" + ds := dal.Dialect.Select( + t.Col("id"), + t.Col("date"), + t.Col("description"), + t.Col("amount"), + tc.Col("name"), + ). + From(t). + LeftJoin(tc, goqu.On(t.Col("id").Eq(tc.Col("id")))). + Where(t.Col("id").Eq(transactionId)) - rows, err := dal.Db.Query(stmt, transactionId) + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + log.Panic(err) + } + + rows, err := dal.Db.Query(sqlStr, args...) if err != nil { return nil, err } @@ -51,27 +70,30 @@ func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Tr log.Panic("database not available") } - stmts := []string{ - "SELECT t.id, t.date, t.description, t.amount, tc.name, count(*) OVER() AS total_count", - "FROM pfbudget.transactions t", - "LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id", - } - args := []any{limit, offset} + ds := dal.Dialect.Select( + t.Col("id"), + t.Col("date"), + t.Col("description"), + t.Col("amount"), + tc.Col("name"), + goqu.L("count(*) OVER() AS total_count"), + ). + From(t). + LeftJoin(tc, goqu.On(t.Col("id").Eq(tc.Col("id")))). + Order(t.Col("date").Desc()). + Limit(uint(limit)). + Offset(uint(offset)) if category != nil { - stmts = append(stmts, "WHERE tc.name SIMILAR TO '%' || $3 || '%'") - args = append(args, *category) + ds = ds.Where(tc.Col("name").ILike(*category)) } - stmts = append(stmts, - "ORDER BY t.date DESC", - "LIMIT $1", - "OFFSET $2", - ) - stmt := strings.Join(stmts, "\n") + "\n" - log.Printf("DAL::Transactions::stmt: %s", stmt) + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return nil, 0, err + } - rows, err := dal.Db.Query(stmt, args...) + rows, err := dal.Db.Query(sqlStr, args...) if err != nil { return nil, 0, err } @@ -92,33 +114,39 @@ func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Tr var ans entity.Transactions for _, twc := range transactions_with_count { - t := entity.Transaction{Id: twc.Id, Date: twc.Date, Description: twc.Description, Value: twc.Value, Category: twc.Category} - ans = append(ans, t) + tx := entity.Transaction{Id: twc.Id, Date: twc.Date, Description: twc.Description, Value: twc.Value, Category: twc.Category} + ans = append(ans, tx) } return ans, transactions_with_count[0].TotalCount, nil } -func (dal *DalImpl) InsertTransaction(t entity.Transaction) (entity.Transaction, error) { +func (dal *DalImpl) InsertTransaction(tx entity.Transaction) (entity.Transaction, error) { log.Print("DAL::InsertTransaction") if dal.Db == nil { log.Panic("database not available") } - stmts := []string{ - "INSERT INTO pfbudget.transactions (date, description, amount)", - "VALUES ($1, $2, $3)", - "RETURNING id", - } - stmt := strings.Join(stmts, "\n") + "\n" + ds := dal.Dialect.Insert(t).Rows( + goqu.Record{ + "date": tx.Date, + "description": tx.Description, + "amount": tx.Value, + }). + Returning(t.Col("id")) - id := new(uint64) - if err := dal.Db.QueryRow(stmt, t.Date, t.Description, t.Value).Scan(id); err != nil { + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { return entity.Transaction{}, err } - t.Id = *id - return t, nil + id := new(uint64) + if err := dal.Db.QueryRow(sqlStr, args...).Scan(id); err != nil { + return entity.Transaction{}, err + } + + tx.Id = *id + return tx, nil } func (dal *DalImpl) UpdateTransaction(id entity.TransactionId, category *entity.CategoryName) (bool, error) { @@ -132,14 +160,17 @@ func (dal *DalImpl) UpdateTransaction(id entity.TransactionId, category *entity. return false, errors.New("missing category") } - stmts := []string{ - "UPDATE pfbudget.transactions_categorized", - "SET name = $2", - "WHERE id = $1", - } - stmt := strings.Join(stmts, "\n") + "\n" + // TODO(43): implement upsert logic + ds := dal.Dialect.Update(tc). + Set(goqu.Record{"name": *category}). + Where(tc.Col("id").Eq(id)) - result, err := dal.Db.Exec(stmt, id, *category) + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return false, err + } + + result, err := dal.Db.Exec(sqlStr, args...) if err != nil { return false, err } @@ -164,18 +195,15 @@ func (dal *DalImpl) TransactionExists(id uint64) (bool, error) { log.Panic("database not available") } - stmts := []string{ - "SELECT EXISTS(", - " SELECT 1", - " FROM pfbudget.transactions", - " WHERE id = $1", - ")", + ds := dal.Dialect.Select(goqu.L("EXISTS(SELECT 1 FROM pfbudget.transactions WHERE id = ?)", id)) + + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return false, err } - stmt := strings.Join(stmts, "\n") + "\n" exists := new(bool) - err := dal.Db.QueryRow(stmt, id).Scan(&exists) - if err != nil { + if err := dal.Db.QueryRow(sqlStr, args...).Scan(exists); err != nil { return false, err } @@ -189,15 +217,21 @@ func (dal *DalImpl) Bank(bankId string) (*entity.Bank, error) { log.Panic("database not available") } - stmts := []string{ - "SELECT b.name, b.name, n.requisition_id", - "FROM pfbudget.banks b", - "JOIN pfbudget.banks_nordigen n ON b.name = n.name", - "WHERE b.name = $1", - } - stmt := strings.Join(stmts, "\n") + "\n" + ds := dal.Dialect.Select( + b.Col("name"), + b.Col("name"), + bn.Col("requisition_id"), + ). + From(b). + Join(bn, goqu.On(b.Col("name").Eq(bn.Col("name")))). + Where(b.Col("name").Eq(bankId)) - rows, err := dal.Db.Query(stmt, bankId) + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return nil, err + } + + rows, err := dal.Db.Query(sqlStr, args...) if err != nil { return nil, err } @@ -217,14 +251,20 @@ func (dal *DalImpl) Banks() (entity.Banks, error) { log.Panic("database not available") } - stmts := []string{ - "SELECT b.name, b.name, n.requisition_id", - "FROM pfbudget.banks b", - "JOIN pfbudget.banks_nordigen n ON b.name = n.name", - } - stmt := strings.Join(stmts, "\n") + "\n" + ds := dal.Dialect.Select( + b.Col("name"), + b.Col("name"), + bn.Col("requisition_id"), + ). + From(b). + Join(bn, goqu.On(b.Col("name").Eq(bn.Col("name")))) - rows, err := dal.Db.Query(stmt) + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return entity.Banks{}, err + } + + rows, err := dal.Db.Query(sqlStr, args...) if err != nil { return entity.Banks{}, err } @@ -239,13 +279,18 @@ func (dal *DalImpl) Categories() (entity.Categories, error) { log.Panic("database not available") } - stmts := []string{ - "SELECT c.name, c.group", - "FROM pfbudget.categories c", - } - stmt := strings.Join(stmts, "\n") + "\n" + ds := dal.Dialect.Select( + c.Col("name"), + c.Col("group"), + ). + From(c) - rows, err := dal.Db.Query(stmt) + sqlStr, args, err := ds.Prepared(true).ToSQL() + if err != nil { + return []entity.Category{}, err + } + + rows, err := dal.Db.Query(sqlStr, args...) if err != nil { return []entity.Category{}, err } diff --git a/internal/dal/impl_test.go b/internal/dal/impl_test.go index e275428..8fa5400 100644 --- a/internal/dal/impl_test.go +++ b/internal/dal/impl_test.go @@ -66,12 +66,7 @@ func TestDalImpl_Transaction(t *testing.T) { } mock. - ExpectQuery(` - ^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name - FROM \w+\.transactions \w+ - LEFT JOIN \w+\.transactions_categorized \w+ - ON \w+\.id = \w+\.id - WHERE \w+\.id = \$1$`). + ExpectQuery("^SELECT.*id.*date.*description.*amount.*name.*WHERE.*id.*= \\?"). WithArgs(tt.args.transactionId). WillReturnRows( mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...), @@ -114,7 +109,7 @@ func TestDalImpl_Transactions(t *testing.T) { { "SelectTransactions", fields{db}, - args{limit: 30, offset: 0, category: nil}, + args{limit: 30, offset: 30, category: nil}, [][]driver.Value{ {1, date, "income", 1000, nil, 2}, {2, date, "expense", -10.50, nil, 2}, @@ -128,7 +123,7 @@ func TestDalImpl_Transactions(t *testing.T) { { "SelectTransactionsWithCategory", fields{db}, - args{limit: 30, offset: 0, category: golang.Ptr("C1")}, + args{limit: 30, offset: 30, category: golang.Ptr("C1")}, [][]driver.Value{ {1, date, "income", 1000, "C1", 2}, {2, date, "expense", -10.50, nil, 2}, @@ -140,7 +135,7 @@ func TestDalImpl_Transactions(t *testing.T) { false, }, { - "SelectNoTransactions", fields{db}, args{limit: 30, offset: 0, category: nil}, nil, nil, false, + "SelectNoTransactions", fields{db}, args{limit: 30, offset: 30, category: nil}, nil, nil, false, }, } for _, tt := range tests { @@ -149,22 +144,14 @@ func TestDalImpl_Transactions(t *testing.T) { Db: tt.fields.Db, } - args := []driver.Value{tt.args.limit, tt.args.offset} - stmt := ` - ^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name, count\(\*\) OVER\(\) AS total_count - FROM \w+\.transactions \w+ - LEFT JOIN \w+\.transactions_categorized \w+ - ON \w+\.id = \w+\.id - ` + args := []driver.Value{} + stmt := "^SELECT.*id.*date.*description.*amount.*name.*count\\(\\*\\) OVER\\(\\) AS total_count" if tt.args.category != nil { - stmt += `WHERE \w+\.name SIMILAR TO '%' || \$3 || '%'` + stmt += ".*WHERE.*name.*ILIKE \\?" args = append(args, *tt.args.category) } - stmt += ` - ORDER BY \w+\.date DESC - LIMIT \$1 - OFFSET \$2 - ` + stmt += ".*ORDER BY.*date.*DESC LIMIT \\? OFFSET \\?" + args = append(args, tt.args.limit, tt.args.offset) mock. ExpectQuery(stmt). @@ -227,7 +214,7 @@ func TestDalImpl_Bank(t *testing.T) { } mock. - ExpectQuery("^SELECT .* FROM .*banks b JOIN .*banks_nordigen n ON b.name = n.name WHERE b.name = \\$1$"). + ExpectQuery("^SELECT.*name.*name.*requisition_id.*WHERE.*name.*= \\?"). WithArgs(tt.args.bankId). WillReturnRows( mock. @@ -290,7 +277,7 @@ func TestDalImpl_Banks(t *testing.T) { } mock. - ExpectQuery("^SELECT .* FROM .*banks b JOIN .*banks_nordigen n ON b.name = n.name$"). + ExpectQuery("^SELECT.*name.*name.*requisition_id"). WithoutArgs(). WillReturnRows( mock. @@ -346,10 +333,10 @@ func TestDalImpl_InsertTransaction(t *testing.T) { mock. ExpectQuery(` - INSERT INTO .* \(date, description, amount\) - VALUES \(\$1, \$2, \$3\) - RETURNING id`). - WithArgs(tt.args.t.Date, tt.args.t.Description, tt.args.t.Value). + INSERT INTO.*.*amount.*date.*description.* + VALUES.*\?, \?, \?.* + RETURNING.*id`). + WithArgs(tt.args.t.Value, tt.args.t.Date, tt.args.t.Description). WillReturnRows( mock.NewRows([]string{"id"}). AddRows([]driver.Value{tt.want.Id}), @@ -413,10 +400,10 @@ func TestDalImpl_UpdateTransaction(t *testing.T) { mock. ExpectExec(` - UPDATE \w+\.transactions_categorized - SET name = \$2 - WHERE id = \$1`). - WithArgs(tt.args.id, tt.args.category). + UPDATE.*transactions_categorized.* + SET.*name.*= ?.* + WHERE.*id.*= ?`). + WithArgs(tt.args.category, tt.args.id). WillReturnResult(tt.mocks) got, err := dal.UpdateTransaction(tt.args.id, tt.args.category) @@ -460,12 +447,7 @@ func TestDalImpl_TransactionExists(t *testing.T) { } mock. - ExpectQuery(` - SELECT EXISTS\( - SELECT 1 - FROM pfbudget.transactions - WHERE id = \$1 - \)`). + ExpectQuery("EXISTS\\(SELECT 1 FROM pfbudget\\.transactions WHERE id = \\?\\)"). WithArgs(tt.args.id). WillReturnRows( mock.NewRows([]string{"exists"}). @@ -529,7 +511,7 @@ func TestDalImpl_Categories(t *testing.T) { } mock. - ExpectQuery(`SELECT \w+\.name, \w+\.group FROM \w+.categories \w+`). + ExpectQuery(`SELECT.*name.*group.*FROM.*categories`). WithoutArgs(). WillReturnRows(mock.NewRows([]string{"name", "group"}).AddRows(tt.mocks...))