8000 GitHub - jacoelho/qs · GitHub
[go: up one dir, main page]

Skip to content
jacoelhoPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Querysmith (qs)

qs builds PostgreSQL queries from composable Go expressions. You can start with a query, add filters, joins, or pagination, and render SQL with a separate list of arguments for your database driver.

qs renders SQL and nothing else. It does not connect to a database, execute queries, or include a driver: your application supplies the connection, manages transactions, and scans results.

The library supports SELECT, INSERT, UPDATE, DELETE, MERGE, common table expressions (CTEs), joins, set operations, window functions, JSON/JSONB operators, and SQL/JSON syntax including JSON_TABLE. It uses only the Go standard library, with no unsafe code or third-party dependencies. The root package uses no reflection; the projection companion uses reflect when a declaration is made, not when a row is scanned.

When rendering a query, qs quotes identifiers and binds application values to $n parameters. A single renderer numbers those parameters in SQL order across the entire query, including nested queries. Statement builders are immutable values, so a query can be branched and shared between goroutines.

Rendering checks query structure only. Schema objects, SQL types, privileges, and driver encoding are left to PostgreSQL and your driver; see Know what rendering checks.

Install qs

qs requires Go 1.27 or later. To install it in your Go module, run:

go get github.com/jacoelho/qs

Install your database driver separately; qs does not include one.

Build a first query

This program builds a query and prints its SQL and arguments:

package main

import (
    "fmt"

    "github.com/jacoelho/qs"
)

func main() {
    query, args, err := qs.Select("id", "name").
        From("users").
        Where(qs.Col("active").Eq(qs.Param(true)), qs.Col("deleted_at").IsNull()).
        OrderBy(qs.Col("created_at").Desc(), qs.Col("id").Desc()).
        Limit(20).
        ToSQL()
    if err != nil {
        panic(err)
    }
    fmt.Println(query)
    fmt.Println(args)
}

The SQL below uses line breaks for readability:

SELECT "id", "name" FROM "users"
WHERE ("active" = $1) AND ("deleted_at" IS NULL)
ORDER BY "created_at" DESC, "id" DESC LIMIT $2

Arguments: [true 20].

After handling any render error, pass query and args... to your driver:

  • With database/sql, use db.QueryContext(ctx, query, args...).
  • With pgx, use conn.Query(ctx, query, args...).

Know three rules

Keep every result. Builders are immutable values: a method returns a new builder and leaves its receiver unchanged, so write q = q.Where(...). A call whose result is dropped does nothing, and neither the compiler nor rendering reports it; the qsvet vet tool does.

A string is one identifier. From("public.users") renders "public.users", one name with a dot in it. For a qualified name pass each part: qs.Table("public", "users") and qs.Col("u", "id"). Clauses such as Select, From, GroupBy, and Returning take names or these values, one kind per call.

Application values are bound, never written into the SQL. qs.Param(v) binds a value in an expression and qs.Value(v) binds one at a write destination. Trusted SQL constructors such as UnsafeSQL are the only way text reaches the statement unquoted; never pass request text to them.

Bind values and name identifiers covers these rules in full, including how to pass a list built at run time.

Common tasks

Each task below is covered in more depth by the guide it links to.

Add optional filters

Where appends conditions with AND, so ordinary Go control flow adds a filter only when its input is present:

func filteredUsers(tenantID int, prefix string, roles []string) qs.SelectBuilder {
    q := qs.Select("id", "email").
        From("users").
        Where(qs.Col("tenant_id").Eq(qs.Param(tenantID)), qs.Col("deleted_at").IsNull())

    if prefix != "" {
        q = q.Where(qs.Or(
            qs.Col("name").ILikePrefix(prefix),
            qs.Col("email").ILikePrefix(prefix),
        ))
    }
    if len(roles) > 0 {
        q = q.Where(qs.TypedCol[string]("role").In(roles...))
    }
    return q
}
query, args, err := filteredUsers(42, "jo", []string{"admin", "editor"}).ToSQL()
SELECT "id", "email" FROM "users"
WHERE ("tenant_id" = $1)
  AND ("deleted_at" IS NULL)
  AND (("name" ILIKE $2 ESCAPE E'!') OR ("email" ILIKE $3 ESCAPE E'!'))
  AND ("role" IN ($4, $5))

Arguments: [42 jo% jo% admin editor].

See Filter rows for literal text matching, typed fields, and array parameters.

Page results

One base query serves every page, because a derived builder never changes its source:

base := qs.Select("id").From("users").Where(qs.Col("active").Eq(qs.Param(true)))

first := base.OrderBy(qs.Col("id").Asc()).Limit(10).Offset(0)
next := base.OrderBy(qs.Col("id").Asc()).Limit(10).Offset(10)

query, args, err := next.ToSQL()
SELECT "id" FROM "users" WHERE ("active" = $1)
ORDER BY "id" ASC LIMIT $2 OFFSET $3

Arguments: [true 10 10].

See Page results for keyset cursors with SeekAfter, row locks, and CountRows.

Join tables

users := qs.Table("users").As("u")
teams := qs.Table("teams").As("t")

query, args, err := qs.Select(users.Col("id"), teams.Col("name")).
    From(users).
    LeftJoin(teams, teams.Col("id").Eq(users.Col("team_id"))).
    Where(users.Col("active").Eq(qs.Param(true))).
    ToSQL()
SELECT "u"."id", "t"."name"
FROM ("users" AS "u" LEFT JOIN "teams" AS "t" ON ("t"."id" = "u"."team_id"))
WHERE ("u"."active" = $1)

Arguments: [true].

See Compose joins, CTEs, and reports for CTEs, CASE, lateral joins, and set operations.

Insert a row

query, args, err := qs.InsertInto("users").
    Columns("id", "name").
    Values(qs.Value(42), qs.Value("Ana")).
    Returning("id").
    ToSQL()
INSERT INTO "users" ("id", "name") VALUES ($1, $2) RETURNING "id"

Arguments: [42 Ana].

Update a row

query, args, err := qs.Update("users").
    Set(qs.Set("name", qs.Value("Ana"))).
    Where(qs.Col("id").Eq(qs.Param(42))).
    Returning("id", "name").
    ToSQL()
UPDATE "users" SET "name" = $1 WHERE ("id" = $2) RETURNING "id", "name"

Arguments: [Ana 42].

Delete a row

query, args, err := qs.DeleteFrom("users").
    Where(qs.Col("id").Eq(qs.Param(42))).
    Returning("id").
    ToSQL()
DELETE FROM "users" WHERE ("id" = $1) RETURNING "id"

Arguments: [42].

An UPDATE or DELETE has no ToSQL until Where, WhereCurrentOf, or AllRows says which rows it changes, so a mutation that forgot its predicate does not compile.

Update a row on conflict

query, args, err := qs.InsertInto("users").
    Columns("id", "name").
    Values(qs.Value(42), qs.Value("Ana")).
    OnConflict(
        qs.ConflictColumns("id").
            DoUpdate(qs.Set("name", qs.Write(qs.Excluded("name")))),
    ).
    Returning("id").
    ToSQL()
INSERT INTO "users" ("id", "name") VALUES ($1, $2)
ON CONFLICT ("id") DO UPDATE SET "name" = "excluded"."name"
RETURNING "id"

Arguments: [42 Ana].

See Write rows for inserts from slices and arrays, row selection for UPDATE and DELETE, and conflict targets.

Handle a render error

ToSQL returns an error when the statement's structure is invalid. Identify the cause with errors.Is:

_, _, err := qs.Values(1).Row(2, 3).ToSQL()
if errors.Is(err, qs.ErrInvalid) {
    // err: qs: invalid query (VALUES): row 2: expected 1 value, got 2 values [path: VALUES → row[2]]
}

Rendering checks structure only; the database still checks schema, types, and privileges. See Render SQL and handle errors.

Read the guides

Guide Covers
Bind values and name identifiers Param, typed fields, shared parameters, NULL, naming, run-time lists, trusted SQL
Filter rows Optional filters, literal text matching, array parameters
Page results Ordering, limits, keyset cursors, row locks, counts
Write rows INSERT, UPDATE, DELETE, columnar batches, ON CONFLICT
Compose joins, CTEs, and reports CTEs, joins, CASE, lateral joins, set operations
Build and query JSON SQL/JSON constructors, document operators, JSON_TABLE
Scan rows with projection declarations The projection package: declaring and reading result rows
Use PostgreSQL types with pgx Casts, pgx values, composite types
Render SQL and handle errors Render limits, what rendering checks, error causes, buffer reuse
Reuse and share builders Immutability, the qsvet tool, which calls append or replace
Develop qs Checks, code generation, database tests

Find references

  • Executable examples: examples with expected SQL and arguments.
  • go doc .: the full exported API.
  • go doc ./projection: reusable result declarations and their ownership contracts.
  • Architecture guide: boundaries, ownership, and design decisions.
  • PostgreSQL corpus: construction measurements and fixture maintenance.

The pinned corpus holds 28,197 query occurrences from PostgreSQL 18's regression SQL. The public API builds 27,887 of them (98.90%) into SQL that reparses to the source tree; 220 use syntax qs omits by design, 50 are refused at render time and 40 have no construction. This measures SQL construction, not schema validity, query results, or query plans.

qs uses the MIT license. PostgreSQL-derived fixtures retain their upstream notice.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0