{"id":13561668,"url":"https://github.com/pilagod/gorm-cursor-paginator","last_synced_at":"2025-05-15T18:10:34.283Z","repository":{"id":35004982,"uuid":"152229968","full_name":"pilagod/gorm-cursor-paginator","owner":"pilagod","description":"A paginator doing cursor-based pagination based on GORM","archived":false,"fork":false,"pushed_at":"2024-10-16T06:08:36.000Z","size":225,"stargazers_count":199,"open_issues_count":4,"forks_count":44,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-03T06:08:15.406Z","etag":null,"topics":["cursor-pagination","go","golang","gorm","pagination"],"latest_commit_sha":null,"homepage":"https://github.com/pilagod/gorm-cursor-paginator","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pilagod.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-10-09T10:09:20.000Z","updated_at":"2025-03-22T02:40:54.000Z","dependencies_parsed_at":"2024-06-18T15:14:08.454Z","dependency_job_id":"d0389095-b3e6-48d1-b302-8908caee6c37","html_url":"https://github.com/pilagod/gorm-cursor-paginator","commit_stats":null,"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pilagod%2Fgorm-cursor-paginator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pilagod%2Fgorm-cursor-paginator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pilagod%2Fgorm-cursor-paginator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pilagod%2Fgorm-cursor-paginator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pilagod","download_url":"https://codeload.github.com/pilagod/gorm-cursor-paginator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248489275,"owners_count":21112540,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cursor-pagination","go","golang","gorm","pagination"],"created_at":"2024-08-01T13:00:59.784Z","updated_at":"2025-04-11T22:29:36.570Z","avatar_url":"https://github.com/pilagod.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# gorm-cursor-paginator ![Build Status](https://github.com/pilagod/gorm-cursor-paginator/actions/workflows/test.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/pilagod/gorm-cursor-paginator/badge.svg?branch=master\u0026kill_cache=1)](https://coveralls.io/github/pilagod/gorm-cursor-paginator?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/pilagod/gorm-cursor-paginator)](https://goreportcard.com/report/github.com/pilagod/gorm-cursor-paginator)\n\nA paginator doing cursor-based pagination based on [GORM](https://github.com/go-gorm/gorm)\n\n\u003e This doc is for v2, which uses [GORM v2](https://github.com/go-gorm/gorm). If you are using [GORM v1](https://github.com/jinzhu/gorm), please checkout [v1 doc](https://github.com/pilagod/gorm-cursor-paginator/tree/v1).\n\n## Features\n\n- Query extendable.\n- Multiple paging keys.\n- Pagination across custom types (e.g. JSON)\n- Paging rule customization for each key.\n- GORM `column` tag supported.\n- Error handling enhancement.\n- Exporting `cursor` module for advanced usage.\n- Implement custom codec for cursor encoding/decoding.\n\n## Installation\n\n```sh\ngo get -u github.com/pilagod/gorm-cursor-paginator/v2\n```\n\n## Usage By Example\n\n```go\nimport (\n   \"github.com/pilagod/gorm-cursor-paginator/v2/paginator\"\n)\n```\n\nGiven an `User` model for example:\n\n```go\ntype User struct {\n    ID          int\n    JoinedAt    time.Time `gorm:\"column:created_at\"`\n}\n```\n\nWe first need to create a `paginator.Paginator` for `User`, here are some useful patterns:\n\n1. Configure by `paginator.Option`, those functions with `With` prefix are factories for `paginator.Option`:\n\n    ```go\n    func CreateUserPaginator(\n        cursor paginator.Cursor,\n        order *paginator.Order,\n        limit *int,\n    ) *paginator.Paginator {\n        opts := []paginator.Option{\n            \u0026paginator.Config{\n                Keys: []string{\"ID\", \"JoinedAt\"},\n                Limit: 10,\n                Order: paginator.ASC,\n            },\n        }\n        if limit != nil {\n            opts = append(opts, paginator.WithLimit(*limit))\n        }\n        if order != nil {\n            opts = append(opts, paginator.WithOrder(*order))\n        }\n        if cursor.After != nil {\n            opts = append(opts, paginator.WithAfter(*cursor.After))\n        }\n        if cursor.Before != nil {\n            opts = append(opts, paginator.WithBefore(*cursor.Before))\n        }\n        return paginator.New(opts...)\n    }\n    ```\n\n2. Configure by setters on `paginator.Paginator`:\n\n    ```go\n    func CreateUserPaginator(\n        cursor paginator.Cursor,\n        order *paginator.Order,\n        limit *int,\n    ) *paginator.Paginator {\n        p := paginator.New(\n            \u0026paginator.Config{\n                Keys: []string{\"ID\", \"JoinedAt\"},\n                Limit: 10,\n                Order: paginator.ASC,\n            },\n        )\n        if order != nil {\n            p.SetOrder(*order)\n        }\n        if limit != nil {\n            p.SetLimit(*limit)\n        }\n        if cursor.After != nil {\n            p.SetAfterCursor(*cursor.After)\n        }\n        if cursor.Before != nil {\n            p.SetBeforeCursor(*cursor.Before)\n        }\n        return p\n    }\n    ```\n\n3. Configure by `paginator.Rule` for fine grained setting for each key:\n\n    \u003e Please refer to [Specification](#specification) for details of `paginator.Rule`.\n\n    ```go\n    func CreateUserPaginator(/* ... */) {\n        p := paginator.New(\n            \u0026paginator.Config{\n                Rules: []paginator.Rule{\n                    {\n                        Key: \"ID\",\n                    },\n                    {\n                        Key: \"JoinedAt\",\n                        Order: paginator.DESC,\n                        SQLRepr: \"users.created_at\",\n                        NULLReplacement: \"1970-01-01\",\n                    },\n                },\n                Limit: 10,\n                // Order here will apply to keys without order specified.\n                // In this example paginator will order by \"ID\" ASC, \"JoinedAt\" DESC.\n                Order: paginator.ASC, \n            },\n        )\n        // ...\n        return p\n    }\n    ```\n\n4. By default the library encodes cursors with `base64`. If a custom encoding/decoding implementation is required, this can be implemented and passed as part of the configuration:\n\n\nFirst implement your custom codec such that it conforms to the `CursorCodec` interface:\n\n\n```go\ntype CursorCodec interface {\n    // Encode encodes model fields into cursor\n    Encode(\n        fields []pc.EncoderField,\n        model interface{},\n    ) (string, error)\n\n    // Decode decodes cursor into model fields\n    Decode(\n        fields []pc.DecoderField,\n        cursor string,\n        model interface{},\n    ) ([]interface{}, error)\n}\n    \ntype customCodec struct {}\n\nfunc (cc *CustomCodec) Encode(fields []pc.EncoderField, model interface{}) (string, error) {\n    ...\n}\n\nfunc (cc *CustomCodec) Decode(fields []pc.DecoderField, cursor string, model interface{}) ([]interface{}, error) {\n    ...\n}\n```\n\nThen pass an instance of your codec during initialisation:\n\n```go\nfunc CreateUserPaginator(/* ... */) {\n\tcodec := \u0026customCodec{}\n\t\n\tp := paginator.New(\n        \u0026paginator.Config{\n            Rules: []paginator.Rule{\n                {\n                    Key: \"ID\",\n                },\n                {\n                    Key: \"JoinedAt\",\n                    Order: paginator.DESC,\n                    SQLRepr: \"users.created_at\",\n                    NULLReplacement: \"1970-01-01\",\n                },\n            },\n            Limit: 10,\n            // supply a custom implementation for the encoder/decoder \n            CursorCodec: codec,\n            // Order here will apply to keys without order specified.\n            // In this example paginator will order by \"ID\" ASC, \"JoinedAt\" DESC.\n            Order: paginator.ASC, \n        },\n    )\n    // ...\n    return p\n}\n```\n\nAfter knowing how to setup the paginator, we can start paginating `User` with GORM:\n\n```go\nfunc FindUsers(db *gorm.DB, query Query) ([]User, paginator.Cursor, error) {\n    var users []User\n\n    // extend query before paginating\n    stmt := db.\n        Select(/* fields */).\n        Joins(/* joins */).\n        Where(/* queries */)\n\n    // create paginator for User model\n    p := CreateUserPaginator(/* config */)\n\n    // find users with pagination\n    result, cursor, err := p.Paginate(stmt, \u0026users)\n\n    // this is paginator error, e.g., invalid cursor\n    if err != nil {\n        return nil, paginator.Cursor{}, err\n    }\n\n    // this is gorm error\n    if result.Error != nil {\n        return nil, paginator.Cursor{}, result.Error\n    }\n\n    return users, cursor, nil\n}\n```\n\nThe second value returned from `paginator.Paginator.Paginate` is a `paginator.Cursor` struct, which is same as `cursor.Cursor` struct:\n\n```go\ntype Cursor struct {\n    After  *string `json:\"after\" query:\"after\"`\n    Before *string `json:\"before\" query:\"before\"`\n}\n```\n\nThat's all! Enjoy paginating in the GORM world. :tada:\n\n\u003e For more paginating examples, please checkout [example/main.go](https://github.com/pilagod/gorm-cursor-paginator/blob/master/example/main.go) and [paginator/paginator_paginate_test.go](https://github.com/pilagod/gorm-cursor-paginator/blob/master/paginator/paginator_paginate_test.go)\n\u003e\n\u003e For manually encoding/decoding cursor exmaples, please check out [cursor/encoding_test.go](https://github.com/pilagod/gorm-cursor-paginator/blob/master/cursor/encoding_test.go)\n\n## Specification\n\n### paginator.Paginator\n\nDefault options used by paginator when not specified:\n\n- `Keys`: `[]string{\"ID\"}`\n\n- `Limit`: `10`\n\n- `Order`: `paginator.DESC`\n\n- `AllowTupleCmp`: `paginator.FALSE`\n\nWhen cursor uses more than one key/rule, paginator instances by default generate SQL that is compatible with almost all database management systems. But this query can be very inefficient and can result in a lot of database scans even when proper indices are in place. By enabling the `AllowTupleCmp` option, paginator will emit a slightly different SQL query when all cursor keys are ordered in the same way.\n\nFor example, let us assume we have the following code:\n\n```go\npaginator.New(\n    paginator.WithKeys([]string{\"CreatedAt\", \"ID\"}),\n    paginator.WithAfter(after),\n    paginator.WithLimit(3),\n).Paginate(db, \u0026result)\n```\n\nThe query that hits our database in this case would look something like this:\n\n```sql\n  SELECT *\n    FROM orders\n   WHERE orders.created_at \u003e $1\n      OR orders.created_at = $2 AND orders.id \u003e $3\nORDER BY orders.created_at ASC, orders.id ASC\n   LIMIT 4\n```\n\nEven if we index our table on `(created_at, id)` columns, some database engines will still perform at least full index scan to get to the items we need. And this is the primary use case for tuple comparison optimization. If we enable optimization, our code would look something like this:\n\n```go\npaginator.New(\n    paginator.WithKeys([]string{\"CreatedAt\", \"ID\"}),\n    paginator.WithAfter(after),\n    paginator.WithLimit(3),\n    paginator.WithAllowTupleCmp(paginate.TRUE),\n).Paginate(db, \u0026result)\n```\n\nThe query that hits our database now looks something like this:\n\n```sql\n  SELECT *\n    FROM orders\n   WHERE (orders.created_at, orders.id) \u003e ($1, $2)\nORDER BY orders.created_at ASC, orders.id ASC\n   LIMIT 4\n```\n\nIn this case, if we have index on `(created_at, id)` columns, most DB engines will know how to optimize this query into a simple initial index lookup + scan, making cursor overhead negligible.\n\n### paginator.Rule\n\n- `Key`: Field name in target model struct.\n\n- `Order`: Order for this key only.\n\n- `SQLRepr`: SQL representation used in raw SQL query.\n    \u003e This is especially useful when you have `JOIN` or table alias in your SQL query. If `SQLRepr` is not specified, paginator will get table name from model, plus table key derived by below rules to form the SQL query:\n    \u003e 1. Find GORM tag `column` on struct field.\n    \u003e 2. If tag not found, convert struct field name to snake case.\n\n- `SQLType`: SQL type used for type casting in the raw SQL query.\n    \u003e This is especially useful when working with custom types (e.g. JSON).\n\n- `NULLReplacement`(v2.2.0): Replacement for NULL value when paginating by nullable column.\n    \u003e If you paginate by nullable column, you will encounter [NULLS { FIRST | LAST } problems](https://learnsql.com/blog/how-to-order-rows-with-nulls/). This option let you decide how to order rows with NULL value. For instance, we can set this value to `1970-01-01` for a nullable `date` column, to ensure rows with NULL date will be placed at head when order is ASC, or at tail when order is DESC.\n\n- `CustomType`: Extra information needed only when paginating across custom types (e.g. JSON). To support custom type pagination, the type needs to implement the `CustomType` interface:\n\n  ```go\n  type CustomType interface {\n      // GetCustomTypeValue returns the value corresponding to the meta attribute inside the custom type.\n      GetCustomTypeValue(meta interface{}) (interface{}, error)\n  }\n  ```\n\n  and provide the following information:\n\n  - `Meta`: meta attribute inside the custom type. The paginator will pass this meta attribute to the `GetCustomTypeValue` function, which should return the actual value corresponding to the meta attribute. For JSON, meta would contain the JSON key of the element inside JSON to be used for pagination.\n\n  - `Type`: GoLang type of the meta attribute. \n\n  Also, when paginating across custom types, it is expected that the `SQLRepr` \u0026 `SQLType` are set.  `SQLRepr` should contain the SQL query to get the meta attribute value, while `SQLType` should be used for type casting if needed. Check examples of [JSON custom type](https://github.com/pilagod/gorm-cursor-paginator/blob/c91935c7488bf9907902c8005f429e719cefa96b/paginator/paginator_test.go#L65-L81) and [custom type setting](https://github.com/pilagod/gorm-cursor-paginator/blob/c91935c7488bf9907902c8005f429e719cefa96b/paginator/paginator_paginate_test.go#L567-L617).\n\n## Changelog\n\n### v2.6.1\n\n- Fix slice encoding ([#64](https://github.com/pilagod/gorm-cursor-paginator/pull/64)), credit to [@chrisroberts](https://github.com/chrisroberts).\n\n### v2.6.0\n\n- Add flag `AllowTupleCmp` to enable SQL tuple comparison for performance optimization of composite cursors ([#62](https://github.com/pilagod/gorm-cursor-paginator/pull/62)), credit to [@tadeboro](https://github.com/tadeboro).\n\n### v2.5.0\n\n- Export `GetCursorEncoder` \u0026 `GetCursorDecoder` on `paginator.Paginator` ([#59](https://github.com/pilagod/gorm-cursor-paginator/issues/59)).\n\n### v2.4.2\n\n- Support `NULLReplacement` for custom types ([#58](https://github.com/pilagod/gorm-cursor-paginator/pull/58)), credit to [@zitnik](https://github.com/zitnik).\n\n### v2.4.1\n\n- Cast `NULLReplacement` when `SQLType` is specified ([#52](https://github.com/pilagod/gorm-cursor-paginator/pull/52)), credit to [@jpugliesi](https://github.com/jpugliesi).\n\n### v2.4.0\n\n- Support [NamingStrategy](https://gorm.io/docs/gorm_config.html#NamingStrategy) ([#49](https://github.com/pilagod/gorm-cursor-paginator/pull/49)), credit to [@goxiaoy](https://github.com/goxiaoy).\n\n### v2.3.0\n\n- Add `CustomType` to `paginator.Rule` to support [custom data types](https://gorm.io/docs/data_types.html), credit to [@nikicc](https://github.com/nikicc).\n\u003e There are some adjustments to the signatures of `cursor.NewEncoder` and `cursor.NewDecoder`. Be careful when upgrading if you use them directly.\n\n### v2.2.0\n\n- Add `NULLReplacement` to `paginator.Rule` to overcome [NULLS { FIRST | LAST } problems](https://learnsql.com/blog/how-to-order-rows-with-nulls/), credit to [@nikicc](https://github.com/nikicc).\n\n### v2.1.0\n\n- Let client control context, suggestion from [@khalilsarwari](https://github.com/khalilsarwari).\n\n### v2.0.1\n\n- Fix order flip bug when paginating backward, credit to [@sylviamoss](https://github.com/sylviamoss).\n\n## License\n\n© Cyan Ho (pilagod), 2018-NOW\n\nReleased under the [MIT License](https://github.com/pilagod/gorm-cursor-paginator/blob/master/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpilagod%2Fgorm-cursor-paginator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpilagod%2Fgorm-cursor-paginator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpilagod%2Fgorm-cursor-paginator/lists"}