diff --git a/goextVersion.go b/goextVersion.go index 9180327..231f662 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.371" +const GoextVersion = "0.0.372" -const GoextVersionTimestamp = "2024-01-13T14:19:19+0100" +const GoextVersionTimestamp = "2024-01-13T21:36:47+0100" diff --git a/pagination/filter.go b/pagination/filter.go index 38fc227..32add0c 100644 --- a/pagination/filter.go +++ b/pagination/filter.go @@ -5,7 +5,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) -type Filter interface { +type MongoFilter interface { FilterQuery() mongo.Pipeline Sort() bson.D } @@ -23,6 +23,6 @@ func (d dynamicFilter) Sort() bson.D { return d.sort } -func CreateFilter(pipeline mongo.Pipeline, sort bson.D) Filter { +func CreateFilter(pipeline mongo.Pipeline, sort bson.D) MongoFilter { return dynamicFilter{pipeline: pipeline, sort: sort} } diff --git a/sq/paginate.go b/sq/paginate.go new file mode 100644 index 0000000..9f25f2b --- /dev/null +++ b/sq/paginate.go @@ -0,0 +1,126 @@ +package sq + +import ( + "context" + "fmt" + ct "gogs.mikescher.com/BlackForestBytes/goext/cursortoken" + "gogs.mikescher.com/BlackForestBytes/goext/exerr" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" +) + +type PaginateFilter interface { + SQL(params PP) (filterClause string, joinClause string, joinTables []string) + Sort() []FilterSort +} + +type FilterSort struct { + Field string + Direction ct.SortDirection +} + +func Paginate[TData any](ctx context.Context, q Queryable, table string, filter PaginateFilter, scanMode StructScanMode, scanSec StructScanSafety, page int, limit *int) ([]TData, pag.Pagination, error) { + prepParams := PP{} + + sortOrder := filter.Sort() + sortCond := "" + if len(sortOrder) > 0 { + sortCond = "ORDER BY " + for i, v := range sortOrder { + if i > 0 { + sortCond += ", " + } + sortCond += v.Field + " " + string(v.Direction) + } + } + + pageCond := "" + if limit != nil { + pageCond += fmt.Sprintf("LIMIT :%s OFFSET :%s", prepParams.Add(*limit+1), prepParams.Add(*limit*(page-1))) + } + + filterCond, joinCond, joinTables := filter.SQL(prepParams) + + selectCond := table + ".*" + for _, v := range joinTables { + selectCond += ", " + v + ".*" + } + + sqlQueryData := "SELECT " + selectCond + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " ) " + sortCond + " " + pageCond + sqlQueryCount := "SELECT " + "COUNT(*)" + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " ) " + + rows, err := q.Query(ctx, sqlQueryData, prepParams) + if err != nil { + return nil, pag.Pagination{}, exerr.Wrap(err, "failed to list paginated entries from DB").Str("table", table).Any("filter", filter).Int("page", page).Any("limit", limit).Build() + } + + entities, err := ScanAll[TData](ctx, q, rows, scanMode, scanSec, true) + if err != nil { + return nil, pag.Pagination{}, exerr.Wrap(err, "failed to decode paginated entries from DB").Str("table", table).Int("page", page).Any("limit", limit).Str("scanMode", string(scanMode)).Str("scanSec", string(scanSec)).Build() + } + + if page == 1 && (limit == nil || len(entities) <= *limit) { + return entities, pag.Pagination{ + Page: 1, + Limit: langext.Coalesce(limit, len(entities)), + TotalPages: 1, + TotalItems: len(entities), + CurrentPageCount: 1, + }, nil + } else { + + countRows, err := q.Query(ctx, sqlQueryCount, prepParams) + if err != nil { + return nil, pag.Pagination{}, exerr.Wrap(err, "failed to query total-count of paginated entries from DB").Str("table", table).Build() + } + + if !countRows.Next() { + return nil, pag.Pagination{}, exerr.New(exerr.TypeSQLQuery, "SQL COUNT(*) query returned no rows").Str("table", table).Any("filter", filter).Build() //TODO TypeSQLDecode + } + + var countRes int + err = countRows.Scan(&countRes) + if err != nil { + return nil, pag.Pagination{}, exerr.Wrap(err, "failed to decode total-count of paginated entries from DB").Str("table", table).Build() + } + + if len(entities) > *limit { + entities = entities[:*limit] + } + + paginationObj := pag.Pagination{ + Page: page, + Limit: langext.Coalesce(limit, countRes), + TotalPages: pag.CalcPaginationTotalPages(countRes, langext.Coalesce(limit, countRes)), + TotalItems: countRes, + CurrentPageCount: len(entities), + } + + return entities, paginationObj, nil + } +} + +func Count(ctx context.Context, q Queryable, table string, filter PaginateFilter) (int, error) { + prepParams := PP{} + + filterCond, joinCond, _ := filter.SQL(prepParams) + + sqlQueryCount := "SELECT " + "COUNT(*)" + " FROM " + table + " " + joinCond + " WHERE ( " + filterCond + " )" + + countRows, err := q.Query(ctx, sqlQueryCount, prepParams) + if err != nil { + return 0, exerr.Wrap(err, "failed to query count of entries from DB").Str("table", table).Build() + } + + if !countRows.Next() { + return 0, exerr.New(exerr.TypeSQLQuery, "SQL COUNT(*) query returned no rows").Str("table", table).Any("filter", filter).Build() //TODO TypeSQLDecode + } + + var countRes int + err = countRows.Scan(&countRes) + if err != nil { + return 0, exerr.Wrap(err, "failed to decode count of entries from DB").Str("table", table).Build() + } + + return countRes, nil +} diff --git a/wmo/queryPaginate.go b/wmo/queryPaginate.go index bff51ad..7caaf18 100644 --- a/wmo/queryPaginate.go +++ b/wmo/queryPaginate.go @@ -9,7 +9,7 @@ import ( pag "gogs.mikescher.com/BlackForestBytes/goext/pagination" ) -func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.Filter, page int, limit *int) ([]TData, pag.Pagination, error) { +func (c *Coll[TData]) Paginate(ctx context.Context, filter pag.MongoFilter, page int, limit *int) ([]TData, pag.Pagination, error) { type totalCountResult struct { Count int `bson:"count"` }