Loukoum
A simple SQL Query Builder.
Introduction
Loukoum is a simple SQL Query Builder, only PostgreSQL is supported at the moment.
If you have to generate complex queries, which rely on various contexts, loukoum is the right tool for you.
Afraid to slip a tiny SQL injection manipulating fmt
to append conditions? Fear no more, loukoum is here to protect you against yourself.
Just a few examples when and where loukoum can become handy:
- Remove user anonymity if user is an admin
- Display news draft for an author
- Add filters in query based on request parameters
- Add a
ON CONFLICT
clause for resource's owner - And so on...
Installation
Using Go Modules
go get github.com/ulule/loukoum/v3@v3.3.0
Usage
Loukoum helps you generate SQL queries from composable parts.
However, keep in mind it's not an ORM or a Mapper so you have to use a SQL connector
(database/sql, sqlx, makroud, etc.) to execute queries.
INSERT
Insert a new Comment
and retrieve its id
.
import lk "github.com/ulule/loukoum/v3"
type Comment struct {
ID int64
Email string `db:"email"`
Status string `db:"status"`
Message string `db:"message"`
UserID int64 `db:"user_id"`
CreatedAt pq.NullTime `db:"created_at"`
DeletedAt pq.NullTime `db:"deleted_at"`
}
func CreateComment(db *sqlx.DB, comment Comment) (Comment, error) {
builder := lk.Insert("comments").
Set(
lk.Pair("email", comment.Email),
lk.Pair("status", "waiting"),
lk.Pair("message", comment.Message),
lk.Pair("created_at", lk.Raw("NOW()")),
).
Returning("id")
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return comment, err
}
defer stmt.Close()
err = stmt.Get(&comment, args)
if err != nil {
return comment, err
}
return comment, nil
}
INSERT on conflict (UPSERT)
import lk "github.com/ulule/loukoum/v3"
func UpsertComment(db *sqlx.DB, comment Comment) (Comment, error) {
builder := lk.Insert("comments").
Set(
lk.Pair("email", comment.Email),
lk.Pair("status", "waiting"),
lk.Pair("message", comment.Message),
lk.Pair("user_id", comment.UserID),
lk.Pair("created_at", lk.Raw("NOW()")),
).
OnConflict("email", lk.DoUpdate(
lk.Pair("message", comment.Message),
lk.Pair("user_id", comment.UserID),
lk.Pair("status", "waiting"),
lk.Pair("created_at", lk.Raw("NOW()")),
lk.Pair("deleted_at", nil),
)).
Returning("id, created_at")
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return comment, err
}
defer stmt.Close()
err = stmt.Get(&comment, args)
if err != nil {
return comment, err
}
return comment, nil
}
UPDATE
Publish a News
by updating its status and publication date.
type News struct {
ID int64
Status string `db:"status"`
PublishedAt pq.NullTime `db:"published_at"`
DeletedAt pq.NullTime `db:"deleted_at"`
}
func PublishNews(db *sqlx.DB, news News) (News, error) {
builder := lk.Update("news").
Set(
lk.Pair("published_at", lk.Raw("NOW()")),
lk.Pair("status", "published"),
).
Where(lk.Condition("id").Equal(news.ID)).
And(lk.Condition("deleted_at").IsNull(true)).
Returning("published_at")
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return news, err
}
defer stmt.Close()
err = stmt.Get(&news, args)
if err != nil {
return news, err
}
return news, nil
}
SELECT
Basic SELECT with an unique condition
Retrieve non-deleted users.
import lk "github.com/ulule/loukoum/v3"
type User struct {
ID int64
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Email string
IsStaff bool `db:"is_staff"`
DeletedAt pq.NullTime `db:"deleted_at"`
}
func FindUsers(db *sqlx.DB) ([]User, error) {
builder := lk.Select("id", "first_name", "last_name", "email").
From("users").
Where(lk.Condition("deleted_at").IsNull(true))
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return nil, err
}
defer stmt.Close()
users := []User{}
err = stmt.Select(&users, args)
if err != nil {
return nil, err
}
return users, nil
}
SELECT IN with subquery
Retrieve comments only sent by staff users, the staff users query will be a subquery
as we don't want to use any JOIN operations.
func FindStaffComments(db *sqlx.DB, comment Comment) ([]Comment, error) {
builder := lk.Select("id", "email", "status", "user_id", "message", "created_at").
From("comments").
Where(lk.Condition("deleted_at").IsNull(true)).
Where(
lk.Condition("user_id").In(
lk.Select("id").
From("users").
Where(lk.Condition("is_staff").Equal(true)),
),
)
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return nil, err
}
defer stmt.Close()
comments := []Comment{}
err = stmt.Select(&comments, args)
if err != nil {
return nil, err
}
return comments, nil
}
SELECT with JOIN
Retrieve non-deleted comments sent by a user with embedded user in results.
First, we need to update the Comment
struct to embed User
.
type Comment struct {
ID int64
Email string `db:"email"`
Status string `db:"status"`
Message string `db:"message"`
UserID int64 `db:"user_id"`
User *User `db:"users"`
CreatedAt pq.NullTime `db:"created_at"`
DeletedAt pq.NullTime `db:"deleted_at"`
}
Let's create a FindComments
method to retrieve these comments.
In this scenario we will use an INNER JOIN
but loukoum also supports LEFT JOIN
and RIGHT JOIN
.
func FindComments(db *sqlx.DB, comment Comment) ([]Comment, error) {
builder := lk.
Select(
"comments.id", "comments.email", "comments.status",
"comments.user_id", "comments.message", "comments.created_at",
).
From("comments").
Join(lk.Table("users"), lk.On("comments.user_id", "users.id")).
Where(lk.Condition("comments.deleted_at").IsNull(true))
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return nil, err
}
defer stmt.Close()
comments := []Comment{}
err = stmt.Select(&comments, args)
if err != nil {
return nil, err
}
return comments, nil
}
DELETE
Delete a user based on ID.
func DeleteUser(db *sqlx.DB, user User) error {
builder := lk.Delete("users").
Where(lk.Condition("id").Equal(user.ID))
query, args := builder.NamedQuery()
stmt, err := db.PrepareNamed(query)
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(args)
return err
}
See examples directory for more information.
NOTE: For database/sql
, see standard.
Migration
Migrating from v2.x.x
- Migrate from dep to go modules by
replacing the import path
github.com/ulule/loukoum/...
by github.com/ulule/loukoum/v3/...
Migrating from v1.x.x
Inspiration
Thanks
License
This is Free Software, released under the MIT License
.
Loukoum artworks are released under the Creative Commons BY-SA License
.
Contributing
Don't hesitate ;)