{"id":22165974,"url":"https://github.com/alextanhongpin/goql","last_synced_at":"2025-03-24T16:14:58.310Z","repository":{"id":38833201,"uuid":"503977375","full_name":"alextanhongpin/goql","owner":"alextanhongpin","description":"URL querystring to SQL where","archived":false,"fork":false,"pushed_at":"2022-07-01T08:20:24.000Z","size":127,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-29T21:51:37.941Z","etag":null,"topics":["go","golang","qs","querystring","sql"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alextanhongpin.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}},"created_at":"2022-06-16T01:51:31.000Z","updated_at":"2023-08-19T09:40:50.000Z","dependencies_parsed_at":"2022-09-19T11:01:34.737Z","dependency_job_id":null,"html_url":"https://github.com/alextanhongpin/goql","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Fgoql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Fgoql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Fgoql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alextanhongpin%2Fgoql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alextanhongpin","download_url":"https://codeload.github.com/alextanhongpin/goql/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245304869,"owners_count":20593626,"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":["go","golang","qs","querystring","sql"],"created_at":"2024-12-02T05:17:34.584Z","updated_at":"2025-03-24T16:14:58.289Z","avatar_url":"https://github.com/alextanhongpin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# goql [![Go Reference](https://pkg.go.dev/badge/github.com/alextanhongpin/goql.svg)](https://pkg.go.dev/github.com/alextanhongpin/goql)\n\nParse query string to Postgres SQL operators. This library does not generate the SQL statements, but it can be paired with other ORMs to generate dynamic SQL.\n\nRequires `go 1.18+`.\n\nfor strongly-type approach, see rough implementation [here](https://github.com/alextanhongpin/go-learn/blob/master/sql-filter.md).\n\n## Features\n\n- customizable field names, field ops, query string fields, as well as struct tags\n- supports many operators used by Postgres\n- handles conjunction (and/or)\n- handles type conversion from query string to designated struct field's type\n- handles limit/offset\n- handles sorting\n- handles parsing slices for array operators like `IN`, `NOT IN`, `LIKE`, `ILIKE`\n\n## Installation\n\n```bash\ngo get github.com/alextanhongpin/goql\n```\n\n## Basic example\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/alextanhongpin/goql\"\n)\n\ntype Book struct {\n\tAuthor      string\n\tTitle       string\n\tPublishYear int `q:\"publish_year\" sort:\"true\"`\n}\n\nfunc main() {\n\t// Register a new decoder for the type Book.\n\t// Information are extracted from the individual struct\n\t// fields, as well as the struct tag.\n\tdec := goql.NewDecoder[Book]()\n\n\t/*\n\t\tQuery: Find books that are published by 'Robert\n\t\tGreene' which has the keyword 'law' and 'master'\n\t\tpublished after 2010. The results should be ordered\n\t\tdescending nulls last, limited to 10.\n\n\n\t\tSELECT *\n\t\tFROM books\n\t\tWHERE author = 'Robert Greene'\n\t\tAND publish_year \u003e 2010\n\t\tAND title ilike any(array['law', 'master'])\n\t\tORDER BY publish_year desc nullslast\n\t\tLIMIT 10\n\t*/\n\n\tv := make(url.Values)\n\tv.Set(\"author.eq\", \"Robert Greene\")\n\tv.Set(\"publish_year.gt\", \"2010\")\n\tv.Add(\"title.ilike\", \"law\")\n\tv.Add(\"title.ilike\", \"master\")\n\tv.Add(\"sort_by\", \"publish_year.desc.nullslast\")\n\tv.Add(\"limit\", \"10\")\n\n\tf, err := dec.Decode(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor _, and := range f.And {\n\t\tfmt.Println(\"and:\", and)\n\t}\n\n\tfmt.Println(\"limit:\", *f.Limit)\n\tfor _, sort := range f.Sort {\n\t\tfmt.Println(\"sort:\", sort)\n\t}\n}\n```\n\nOutput:\n\n```\nand: author eq \"Robert Greene\"\nand: publish_year gt 2010\nand: title ilike []interface {}{\"law\", \"master\"}\nlimit: 10\nsort: publish_year desc nullslast\n```\n\n## Operators\n\nBasic datatypes (int, float, string, bool, time):\n\n| op       | querystring                                   | sql                                     |\n|----------|-----------------------------------------------|-----------------------------------------|\n| eq       | `name.eq=john appleseed`                        | `name = 'john appleseed'`                 |\n| neq      | `name.neq=john appleseed`                       | `name \u003c\u003e 'john appleseed'`                |\n| lt       | `age.lt=17`                                     | `age \u003c 17`                                |\n| lte      | `age.lte=17`                                    | `age \u003c= 17`                               |\n| gt       | `age.gt=17`                                     | `age \u003e 17`                                |\n| gte      | `age.gte=17`                                    | `age \u003e= 17`                               |\n| like     | `title.like=programming%`                       | `title like 'programming%'`               |\n| ilike    | `title.ilike=programming%`                      | `title ilike 'programming%'`              |\n| notlike  | `title.notlike=programming%`                    | `title not like 'programming%'`           |\n| notilike | `title.notilike=programming%`                   | `title not ilike 'programming%'`          |\n| in       | `hobbies.in=programming\u0026hobbies.in=music`       | `hobbies in ('programming', 'music')`     |\n| notin    | `hobbies.notin=programming\u0026hobbies.notin=music` | `hobbies not in ('programming', 'music')` |\n| is       | `married.is=true`                               | `married is true`                         |\n| isnot    | `married_at.isnot=null`                         | `married_at is not null`                  |\n\n\nSome operators such as `IN`, `LIKE`, `ILIKE` and their negation `NOT` supports multiple values:\n\n| op       | querystring                                         | sql                                                    |\n|----------|-----------------------------------------------------|--------------------------------------------------------|\n| like     | `title.like=programming%\u0026title.like=music%`         | `title like any(array['programming%', 'music%'])`      |\n| ilike    | `title.ilike=programming%\u0026title.ilike=music%`       | `title ilike any(array['programming%', 'music%'])`     |\n| notlike  | `title.notlike=programming%\u0026title.notlike=music%`   | `title not like all(array['programming%', 'music%'])`  |\n| notilike | `title.notilike=programming%\u0026title.notilike=music%` | `title not ilike all(array['programming%', 'music%'])` |\n| in       | `hobbies.in=programming\u0026hobbies.in=music`           | `hobbies in ('programming', 'music')`                  |\n| notin    | `hobbies.notin=programming\u0026hobbies.notin=music`     | `hobbies not in ('programming', 'music')`              |\n\nIf the target type is an `array` [^1], then multiple values are accepted too:\n\n| op       | querystring                                       | sql                                                  |\n|----------|---------------------------------------------------|------------------------------------------------------|\n| eq       | `hobbies.eq=swimming\u0026hobbies.eq=dancing`            | `hobbies = array['swimming', 'dancing']`               |\n| neq      | `hobbies.neq=swimming\u0026hobbies.neq=dancing`          | `hobbies \u003c\u003e array['swimming', 'dancing']`              |\n| lt       | `scores.lt=50\u0026scores.lt=100`                        | `scores \u003c array[10, 100]`                              |\n| lte      | `scores.lte=50\u0026scores.lte=100`                      | `scores \u003c= array[10, 100]`                             |\n| gt       | `scores.gt=50\u0026scores.gt=100`                        | `scores \u003e= array[10, 100]`                             |\n| gte      | `scores.gte=50\u0026scores.gte=100`                      | `scores \u003e= array[10, 100]`                             |\n\n## And/Or\n\n\n`AND/OR` can be chained and nested.\n\n| op  | querystring                                                                 | sql                                                                             |\n|-----|-----------------------------------------------------------------------------|---------------------------------------------------------------------------------|\n| and | `and=age.gt:13\u0026and=age.lt:30\u0026or=and.(name.ilike:alice%,name.notilike:bob%)` | `AND age \u003e 13 AND age \u003c 30 OR (name ilike 'alice%' AND name not ilike 'bob%'))` |\n| or  | `or=and.(height.isnot:null,height.gte:170)`                                 | `OR (height is not null AND height \u003e= 170)`                                     |\n| or  | `or=height.isnot:null\u0026or=height.gte:170`                                | `OR height is not null OR height \u003e= 170`                                        |\n\n## Limit/Offset\n\n\nThe default naming for the limit/offset in querystring is `limit` and `offset`. The query string name can changed by calling:\n\n```go\ndec.SetQueryLimitName(\"_limit\")\ndec.SetQueryOffsetName(\"_offset\")\n```\n\nThe default min/max for the limit is `1` and `20` respectively. To change the limit:\n\n\n```go\ndec.SetLimitRange(5, 100)\n```\n\n| op           | querystring        | sql                 |\n|--------------|--------------------|---------------------|\n| limit        | `limit=20`           | `LIMIT 20`            |\n| offset       | `offset=42`          | `OFFSET 42`           |\n| limit/offset | `limit=20\u0026offset=42` | `LIMIT 20, OFFSET 42` |\n\n## Sort\n\nTo enable sorting for a field, set the struct tag `sort:\"true\"`. E.g.\n\n```go\ntype User struct {\n\tAge int `sort:\"true\"`\n}\n```\n\nThe `sort` struct name can be changed:\n\n```go\ndec.SetSortTag(\"sortable\")\n```\n\nAnd the example above will then be:\n\n```go\ntype User struct {\n\tAge int `sortable:\"true\"`\n}\n```\n\n| op   | querystring             | sql                                            |\n|------|-------------------------|------------------------------------------------|\n| sort | `sort=age`                | `ORDER BY AGE ASC NULLSLAST`                     |\n|      | `sort=age.asc`            | `ORDER BY age ASC NULLSLAST`                     |\n|      | `sort=age.desc`           | `ORDER BY age DESC NULLSFIRST`                   |\n|      | `sort=age.asc.nullsfirst` | `ORDER BY age ASC NULLSFIRST`                    |\n|      | `sort=age.desc.nullslast` | `ORDER BY age DESC NULLSLAST`                    |\n|      | `sort=id.desc\u0026sort=age`   | `ORDER BY id DESC NULLSFIRST, age ASC NULLSLAST` |\n\n## Tags\n\n\n```go\ntype User struct {\n\tAge int `q:\"age\"`\n}\n```\n\nThe default tag name is `q`. To change it:\n\n\n```go\ndec.SetFilterTag(\"filter\")\n```\n\nAnd the example above will be:\n\n```go\ntype User struct {\n\tAge int `filter:\"age\"`\n}\n```\n\n| field                           | tag                | description                                                                                                                                    |\n|---------------------------------|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------|\n| name string `q:\"name\"`          | name               | modifies the query string name                                                                                                                 |\n| MarriedAt time.Time `q:\",\"`     | name               | if no name is specified, it defaults to lower common initialism of the name                                                                    |\n| ID uuid.UUID `q:\",type:uuid\"`   | type:\u003cyour-type\u003e   | specifies the type that would be used by the `parser`                                                                                          |\n| IDs []string `q:\",type:[]uuid\"` | type:[]\u003cyour-type\u003e | specifies an `array` type. `array` types have special operators                                                                                |\n| ID *string `q:\",type:*uuid\"`    | type:*\u003cyour-type\u003e  | specifies a `null` type. `null` types have special operators                                                                                   |\n| ID string `q:\",null\"`           | null               | another approach of specifying `null` types                                                                                                    |\n| ID string `q:\",ops:eq,neq\"`     | ops                | specifies the list of supported ops. In this example, only `id.eq=v` and `id.neq=v` is valid. This can be further overwritten by `dec.SetOps`. |\n\n\nExamples of valid tags:\n\n```go\nq:\",null\"\nq:\"custom_name,null\"\nq:\"custom_name,type:uuid\"\nq:\"custom_name,ops:eq,neq\"\nq:\"custom_name,type:uuid,ops:eq,neq\"\nq:\"custom_name,type:[]uuid\"\nq:\"custom_name,type:[]*uuid\"\nq:\"custom_name,type:[]*uuid,ops:eq,neq,in,notin\"\n```\n\n## Ops\n\nTo customize `ops` for a specific field, either set the struct tag `ops:\u003ccomma-separate-list-of-ops\u003e`, or set it through the method `SetOps`:\n\n```go\n// To allow only `eq` and `neq` for the field `name`:\ndec.SetOps(\"name\", goql.OpEq | goql.OpNeq)\n```\n\n## Parsers\n\nQuery string parameters are string (or list of string). Parsers are responsible for parsing the string to the desired types that are either\n\n1) inferred through the struct field's type through reflection\n2) set at the struct tag through `type:\"yourtype\"`\n\n\nCustom parsers can be registered as follow:\n\n```go\n\ttype Book struct {\n\t\tID uuid.UUID `q:\"id,type:uuid\"` // Register a new type `uuid`.\n\t}\n\n\tid := uuid.New()\n\n\tv := make(url.Values)\n\tv.Set(\"id.eq\", id.String())\n\tv.Add(\"id.in\", id.String()) // Automatically handles conversion for a list of values.\n\tv.Add(\"id.in\", id.String())\n\n\tdec := goql.NewDecoder[Book]()\n\tdec.SetParser(\"uuid\", parseUUID)\n\n\tf, err := dec.Decode(v)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n```\n\nwhere `parseUUID` fulfills the `goql.ParseFn` method definition:\n\n```go\nfunc parseUUID(in string) (any, error) {\n\treturn uuid.Parse(in)\n}\n```\n\n\n## FAQ\n\n\u003e What if I need to filter some fields from `url.Values`?\n\nFilter it manually before passing to `Decode(v url.Values)`.\n\n\n\u003e What if I need to add validation to the values?\n\nCreate a new type (aka `value object), and add a parser for that type which includes validation. Parsers are type-specific.\n\n```go\n// You can edit this code!\n// Click here and start typing.\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/alextanhongpin/goql\"\n)\n\nvar ErrInvalidEmail = errors.New(\"bad email format\")\n\ntype Email string\n\nfunc (e Email) Validate() error {\n\tif !strings.Contains(string(e), \"@\") {\n\t\treturn ErrInvalidEmail\n\t}\n\n\treturn nil\n}\n\ntype User struct {\n\tEmail Email `q:\",type:email\"` // Register a new type \"email\"\n}\n\nfunc main() {\n\tdec := goql.NewDecoder[User]()\n\tdec.SetParser(\"email\", parseEmail)\n\n\tv := make(url.Values)\n\tv.Set(\"email.eq\", \"bad email\") // Register a parser for type \"email\"\n\t_, err := dec.Decode(v)\n\tfmt.Println(err)\n\tfmt.Println(errors.Is(err, ErrInvalidEmail))\n}\n\nfunc parseEmail(in string) (any, error) {\n\temail := Email(in)\n\treturn email, email.Validate()\n}\n```\n\n\n\u003e Why is there no support for keyset/cursor pagination, only limit/offset?\n\nBecause you can construct the cursor pagination directly.\n\n\n## Reference\n\n[^1]: [List of array operators](https://www.postgresql.org/docs/9.1/functions-array.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falextanhongpin%2Fgoql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falextanhongpin%2Fgoql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falextanhongpin%2Fgoql/lists"}