{"id":13564925,"url":"https://github.com/ulule/loukoum","last_synced_at":"2025-04-05T21:10:42.034Z","repository":{"id":29896590,"uuid":"109009425","full_name":"ulule/loukoum","owner":"ulule","description":"A simple SQL Query Builder","archived":false,"fork":false,"pushed_at":"2023-02-28T11:57:19.000Z","size":663,"stargazers_count":322,"open_issues_count":9,"forks_count":13,"subscribers_count":15,"default_branch":"master","last_synced_at":"2025-03-29T20:08:22.365Z","etag":null,"topics":["builder","go","golang","postgres","postgresql","query","query-builder","sql"],"latest_commit_sha":null,"homepage":"","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/ulule.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}},"created_at":"2017-10-31T14:43:10.000Z","updated_at":"2025-03-25T01:53:37.000Z","dependencies_parsed_at":"2024-01-14T03:49:41.222Z","dependency_job_id":"980bca29-308e-49ee-8449-c0c049ebd811","html_url":"https://github.com/ulule/loukoum","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ulule%2Floukoum","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ulule%2Floukoum/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ulule%2Floukoum/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ulule%2Floukoum/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ulule","download_url":"https://codeload.github.com/ulule/loukoum/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247399885,"owners_count":20932880,"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":["builder","go","golang","postgres","postgresql","query","query-builder","sql"],"created_at":"2024-08-01T13:01:38.015Z","updated_at":"2025-04-05T21:10:42.017Z","avatar_url":"https://github.com/ulule.png","language":"Go","readme":"# Loukoum\n\n[![CircleCI][circle-img]][circle-url]\n[![Documentation][godoc-img]][godoc-url]\n![License][license-img]\n\n_A simple SQL Query Builder._\n\n[![Loukoum][loukoum-img]][loukoum-url]\n\n## Introduction\n\nLoukoum is a simple SQL Query Builder, only **PostgreSQL** is supported at the moment.\n\nIf you have to generate complex queries, which rely on various contexts, **loukoum** is the right tool for you.\n\nAfraid to slip a tiny **SQL injection** manipulating `fmt` to append conditions? **Fear no more**, loukoum is here to protect you against yourself.\n\nJust a few examples when and where loukoum can become handy:\n\n- Remove user anonymity if user is an admin\n- Display news draft for an author\n- Add filters in query based on request parameters\n- Add a `ON CONFLICT` clause for resource's owner\n- And so on...\n\n## Installation\n\nUsing [Go Modules](https://github.com/golang/go/wiki/Modules)\n\n```console\ngo get github.com/ulule/loukoum/v3@v3.3.0\n```\n\n## Usage\n\nLoukoum helps you generate SQL queries from composable parts.\n\nHowever, keep in mind it's not an ORM or a Mapper so you have to use a SQL connector\n([database/sql][sql-url], [sqlx][sqlx-url], [makroud][makroud-url], etc.) to execute queries.\n\n### INSERT\n\nInsert a new `Comment` and retrieve its `id`.\n\n```go\nimport lk \"github.com/ulule/loukoum/v3\"\n\n// Comment model\ntype Comment struct {\n\tID        int64\n\tEmail     string      `db:\"email\"`\n\tStatus    string      `db:\"status\"`\n\tMessage   string      `db:\"message\"`\n\tUserID    int64       `db:\"user_id\"`\n\tCreatedAt pq.NullTime `db:\"created_at\"`\n\tDeletedAt pq.NullTime `db:\"deleted_at\"`\n}\n\n// CreateComment creates a comment.\nfunc CreateComment(db *sqlx.DB, comment Comment) (Comment, error) {\n\tbuilder := lk.Insert(\"comments\").\n\t\tSet(\n\t\t\tlk.Pair(\"email\", comment.Email),\n\t\t\tlk.Pair(\"status\", \"waiting\"),\n\t\t\tlk.Pair(\"message\", comment.Message),\n\t\t\tlk.Pair(\"created_at\", lk.Raw(\"NOW()\")),\n\t\t).\n\t\tReturning(\"id\")\n\n\t// query: INSERT INTO comments (created_at, email, message, status, user_id)\n\t//        VALUES (NOW(), :arg_1, :arg_2, :arg_3, :arg_4) RETURNING id\n\t//  args: map[string]interface{}{\n\t//            \"arg_1\": string(comment.Email),\n\t//            \"arg_2\": string(comment.Message),\n\t//            \"arg_3\": string(\"waiting\"),\n\t//            \"arg_4\": string(comment.UserID),\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn comment, err\n\t}\n\tdefer stmt.Close()\n\n\terr = stmt.Get(\u0026comment, args)\n\tif err != nil {\n\t\treturn comment, err\n\t}\n\n\treturn comment, nil\n}\n```\n\n### INSERT on conflict (UPSERT)\n\n```go\nimport lk \"github.com/ulule/loukoum/v3\"\n\n// UpsertComment inserts or updates a comment based on the email attribute.\nfunc UpsertComment(db *sqlx.DB, comment Comment) (Comment, error) {\n\tbuilder := lk.Insert(\"comments\").\n\t\tSet(\n\t\t\tlk.Pair(\"email\", comment.Email),\n\t\t\tlk.Pair(\"status\", \"waiting\"),\n\t\t\tlk.Pair(\"message\", comment.Message),\n\t\t\tlk.Pair(\"user_id\", comment.UserID),\n\t\t\tlk.Pair(\"created_at\", lk.Raw(\"NOW()\")),\n\t\t).\n\t\tOnConflict(\"email\", lk.DoUpdate(\n\t\t\tlk.Pair(\"message\", comment.Message),\n\t\t\tlk.Pair(\"user_id\", comment.UserID),\n\t\t\tlk.Pair(\"status\", \"waiting\"),\n\t\t\tlk.Pair(\"created_at\", lk.Raw(\"NOW()\")),\n\t\t\tlk.Pair(\"deleted_at\", nil),\n\t\t)).\n\t\tReturning(\"id, created_at\")\n\n\t// query: INSERT INTO comments (created_at, email, message, status, user_id)\n\t//        VALUES (NOW(), :arg_1, :arg_2, :arg_3, :arg_4)\n\t//        ON CONFLICT (email) DO UPDATE SET created_at = NOW(), deleted_at = NULL, message = :arg_5,\n\t//        status = :arg_6, user_id = :arg_7 RETURNING id, created_at\n\t//  args: map[string]interface{}{\n\t//            \"arg_1\": string(comment.Email),\n\t//            \"arg_2\": string(comment.Message),\n\t//            \"arg_3\": string(\"waiting\"),\n\t//            \"arg_4\": string(comment.UserID),\n\t//            \"arg_5\": string(comment.Message),\n\t//            \"arg_6\": string(\"waiting\"),\n\t//            \"arg_7\": string(comment.UserID),\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn comment, err\n\t}\n\tdefer stmt.Close()\n\n\terr = stmt.Get(\u0026comment, args)\n\tif err != nil {\n\t\treturn comment, err\n\t}\n\n\treturn comment, nil\n}\n```\n\n### UPDATE\n\nPublish a `News` by updating its status and publication date.\n\n```go\n// News model\ntype News struct {\n\tID          int64\n\tStatus      string      `db:\"status\"`\n\tPublishedAt pq.NullTime `db:\"published_at\"`\n\tDeletedAt   pq.NullTime `db:\"deleted_at\"`\n}\n\n// PublishNews publishes a news.\nfunc PublishNews(db *sqlx.DB, news News) (News, error) {\n\tbuilder := lk.Update(\"news\").\n\t\tSet(\n\t\t\tlk.Pair(\"published_at\", lk.Raw(\"NOW()\")),\n\t\t\tlk.Pair(\"status\", \"published\"),\n\t\t).\n\t\tWhere(lk.Condition(\"id\").Equal(news.ID)).\n\t\tAnd(lk.Condition(\"deleted_at\").IsNull(true)).\n\t\tReturning(\"published_at\")\n\n\t// query: UPDATE news SET published_at = NOW(), status = :arg_1 WHERE ((id = :arg_2) AND (deleted_at IS NULL))\n\t//        RETURNING published_at\n\t//  args: map[string]interface{}{\n\t//            \"arg_1\": string(\"published\"),\n\t//            \"arg_2\": int64(news.ID),\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn news, err\n\t}\n\tdefer stmt.Close()\n\n\terr = stmt.Get(\u0026news, args)\n\tif err != nil {\n\t\treturn news, err\n\t}\n\n\treturn news, nil\n}\n```\n\n### SELECT\n\n#### Basic SELECT with an unique condition\n\nRetrieve non-deleted users.\n\n```go\nimport lk \"github.com/ulule/loukoum/v3\"\n\n// User model\ntype User struct {\n\tID int64\n\n\tFirstName string `db:\"first_name\"`\n\tLastName  string `db:\"last_name\"`\n\tEmail     string\n\tIsStaff   bool        `db:\"is_staff\"`\n\tDeletedAt pq.NullTime `db:\"deleted_at\"`\n}\n\n// FindUsers retrieves non-deleted users\nfunc FindUsers(db *sqlx.DB) ([]User, error) {\n\tbuilder := lk.Select(\"id\", \"first_name\", \"last_name\", \"email\").\n\t\tFrom(\"users\").\n\t\tWhere(lk.Condition(\"deleted_at\").IsNull(true))\n\n\t// query: SELECT id, first_name, last_name, email FROM users WHERE (deleted_at IS NULL)\n\t//  args: map[string]interface{}{\n\t//\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer stmt.Close()\n\n\tusers := []User{}\n\n\terr = stmt.Select(\u0026users, args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn users, nil\n}\n```\n\n#### SELECT IN with subquery\n\nRetrieve comments only sent by staff users, the staff users query will be a subquery\nas we don't want to use any JOIN operations.\n\n```go\n// FindStaffComments retrieves comments by staff users.\nfunc FindStaffComments(db *sqlx.DB, comment Comment) ([]Comment, error) {\n\tbuilder := lk.Select(\"id\", \"email\", \"status\", \"user_id\", \"message\", \"created_at\").\n\t\tFrom(\"comments\").\n\t\tWhere(lk.Condition(\"deleted_at\").IsNull(true)).\n\t\tWhere(\n\t\t\tlk.Condition(\"user_id\").In(\n\t\t\t\tlk.Select(\"id\").\n\t\t\t\t\tFrom(\"users\").\n\t\t\t\t\tWhere(lk.Condition(\"is_staff\").Equal(true)),\n\t\t\t),\n\t\t)\n\n\t// query: SELECT id, email, status, user_id, message, created_at\n\t//        FROM comments WHERE ((deleted_at IS NULL) AND\n\t//        (user_id IN (SELECT id FROM users WHERE (is_staff = :arg_1))))\n\t//  args: map[string]interface{}{\n\t//            \"arg_1\": bool(true),\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer stmt.Close()\n\n\tcomments := []Comment{}\n\n\terr = stmt.Select(\u0026comments, args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn comments, nil\n}\n```\n\n#### SELECT with JOIN\n\nRetrieve non-deleted comments sent by a user with embedded user in results.\n\nFirst, we need to update the `Comment` struct to embed `User`.\n\n```go\n// Comment model\ntype Comment struct {\n\tID        int64\n\tEmail     string      `db:\"email\"`\n\tStatus    string      `db:\"status\"`\n\tMessage   string      `db:\"message\"`\n\tUserID    int64       `db:\"user_id\"`\n\tUser      *User       `db:\"users\"`\n\tCreatedAt pq.NullTime `db:\"created_at\"`\n\tDeletedAt pq.NullTime `db:\"deleted_at\"`\n}\n```\n\nLet's create a `FindComments` method to retrieve these comments.\n\nIn this scenario we will use an `INNER JOIN` but loukoum also supports `LEFT JOIN` and `RIGHT JOIN`.\n\n```go\n// FindComments retrieves comments by users.\nfunc FindComments(db *sqlx.DB, comment Comment) ([]Comment, error) {\n\tbuilder := lk.\n\t\tSelect(\n\t\t\t\"comments.id\", \"comments.email\", \"comments.status\",\n\t\t\t\"comments.user_id\", \"comments.message\", \"comments.created_at\",\n\t\t).\n\t\tFrom(\"comments\").\n\t\tJoin(lk.Table(\"users\"), lk.On(\"comments.user_id\", \"users.id\")).\n\t\tWhere(lk.Condition(\"comments.deleted_at\").IsNull(true))\n\n\t// query: SELECT comments.id, comments.email, comments.status, comments.user_id, comments.message,\n\t//        comments.created_at FROM comments INNER JOIN users ON comments.user_id = users.id\n\t//        WHERE (comments.deleted_at IS NULL)\n\t//  args: map[string]interface{}{\n\t//\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer stmt.Close()\n\n\tcomments := []Comment{}\n\n\terr = stmt.Select(\u0026comments, args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn comments, nil\n}\n```\n\n### DELETE\n\nDelete a user based on ID.\n\n```go\n// DeleteUser deletes a user.\nfunc DeleteUser(db *sqlx.DB, user User) error {\n\tbuilder := lk.Delete(\"users\").\n\t\tWhere(lk.Condition(\"id\").Equal(user.ID))\n\n\n\t// query: DELETE FROM users WHERE (id = :arg_1)\n\t//  args: map[string]interface{}{\n\t//            \"arg_1\": int64(user.ID),\n\t//        }\n\tquery, args := builder.NamedQuery()\n\n\tstmt, err := db.PrepareNamed(query)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer stmt.Close()\n\n\t_, err = stmt.Exec(args)\n\n\treturn err\n}\n```\n\nSee [examples](examples/named) directory for more information.\n\n\u003e **NOTE:** For `database/sql`, see [standard](examples/standard).\n\n## Migration\n\n### Migrating from v2.x.x\n\n- Migrate from [dep](https://github.com/golang/dep) to [go modules](https://github.com/golang/go/wiki/Modules) by\n  replacing the import path `github.com/ulule/loukoum/...` by `github.com/ulule/loukoum/v3/...`\n\n### Migrating from v1.x.x\n\n- Change `Prepare()` to `NamedQuery()` for [builder.Builder](https://github.com/ulule/loukoum/blob/d6ee7eac818ec74889870fa82dff411ea266463b/builder/builder.go#L19) interface.\n\n## Inspiration\n\n- [squirrel](https://github.com/Masterminds/squirrel)\n- [goqu](https://github.com/doug-martin/goqu)\n- [sqlabble](https://github.com/minodisk/sqlabble)\n\n## Thanks\n\n- [Ilia Choly](https://github.com/icholy)\n\n## License\n\nThis is Free Software, released under the [`MIT License`][software-license-url].\n\nLoukoum artworks are released under the [`Creative Commons BY-SA License`][artwork-license-url].\n\n## Contributing\n\n- Ping us on twitter:\n  - [@novln\\_](https://twitter.com/novln_)\n  - [@oibafsellig](https://twitter.com/oibafsellig)\n  - [@thoas](https://twitter.com/thoas)\n- Fork the [project](https://github.com/ulule/loukoum)\n- Fix [bugs](https://github.com/ulule/loukoum/issues)\n\n**Don't hesitate ;)**\n\n[loukoum-url]: https://github.com/ulule/loukoum\n[loukoum-img]: docs/images/banner.png\n[godoc-url]: https://godoc.org/github.com/ulule/loukoum\n[godoc-img]: https://godoc.org/github.com/ulule/loukoum?status.svg\n[license-img]: https://img.shields.io/badge/license-MIT-blue.svg\n[software-license-url]: LICENSE\n[artwork-license-url]: docs/images/LICENSE\n[sql-url]: https://golang.org/pkg/database/sql/\n[sqlx-url]: https://github.com/jmoiron/sqlx\n[makroud-url]: https://github.com/ulule/makroud/\n[circle-url]: https://circleci.com/gh/ulule/loukoum/tree/master\n[circle-img]: https://circleci.com/gh/ulule/loukoum.svg?style=shield\u0026circle-token=1de7bc4fd603b0df406ceef4bbba3fb3d6b5ed10\n","funding_links":[],"categories":["Go"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fulule%2Floukoum","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fulule%2Floukoum","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fulule%2Floukoum/lists"}