Skip to content

virp/nql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nql

nql is a compact Go package for building SQL queries with bound arguments. It provides mutable builders for SELECT, INSERT, UPDATE, DELETE, plus helper expressions for predicates, aliases, nested queries, and CASE expressions.

The package was inspired by github.com/Masterminds/squirrel. The goal here was to keep a similar query-builder style while avoiding reflection and avoiding wrappers for query execution.

The package follows a simple contract: every builder implements Sqlizer, which renders SQL text and the corresponding []any argument list through ToSql(). This keeps query construction composable without coupling the package to a specific database driver or ORM.

What the package provides

  • SelectBuilder for SELECT queries with support for prefixes, joins, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, suffixes, and subqueries in FROM.
  • InsertBuilder for INSERT and REPLACE, including multi-row VALUES, INSERT ... SELECT, suffixes, and deterministic SetMap column ordering.
  • UpdateBuilder for UPDATE with SET, FROM, WHERE, ORDER BY, LIMIT, OFFSET, suffixes, and subqueries in assignments or FROM.
  • DeleteBuilder for DELETE with WHERE, ORDER BY, LIMIT, OFFSET, prefixes, and suffixes.
  • Expression helpers such as Expr, ConcatExpr, Alias, Case, Eq, NotEq, In, NotIn, Like, ILike, Lt, Gt, And, and Or.
  • Placeholder rewriting from ? to dialect-friendly forms: ?, $1, :1, @p1.

Package characteristics

  • Builders are mutable and accumulate state across method calls. Chained calls update the same builder instance instead of returning a copy, so reusing a builder keeps appending or replacing its existing state. Use Clone() when you need to branch from an existing builder without mutating the original.
  • Nested builders are supported through the shared Sqlizer interface.
  • Map-based predicates and SetMap use sorted keys, so generated SQL is deterministic.
  • Placeholder conversion happens only at the outermost query level, which makes nested builders safe to compose.
  • The package is intentionally lightweight: it only generates SQL and args, and leaves execution to the caller.

Example

Note: builder methods mutate the receiver. Reusing a builder after one chain continues modifying that same query state. Use Clone() when you need branching behavior:

base := nql.Select("id").From("users").Where("active = ?", true)

latest := base.Clone().OrderBy("created_at DESC").Limit(10)
oldest := base.Clone().OrderBy("created_at ASC").Limit(10)

Clone() copies builder-owned slice state so later appends on either builder do not affect the other. Nested Sqlizer values are still shared unless you replace them explicitly.

sql, args, err := nql.Select("u.id", "u.name").
    From("users u").
    LeftJoin("profiles p ON p.user_id = u.id").
    Where(nql.Eq{"u.active": true}).
    Where(nql.In{"u.role": []any{"admin", "editor"}}).
    OrderBy("u.id DESC").
    Limit(20).
    PlaceholderFormat(nql.Dollar).
    ToSql()

Result:

SELECT u.id, u.name
FROM users u
LEFT JOIN profiles p ON p.user_id = u.id
WHERE u.active = $1 AND u.role IN ($2,$3)
ORDER BY u.id DESC
LIMIT 20

Arguments:

[]any{true, "admin", "editor"}

Predicate helpers

The helper types are designed to cover common WHERE and HAVING cases without writing raw SQL for every comparison:

  • Eq and NotEq render equality and null checks.
  • In and NotIn render list predicates.
  • Like, NotLike, ILike, NotILike render string matching predicates.
  • Lt, LtOrEq, Gt, GtOrEq render comparison predicates.
  • And and Or combine multiple Sqlizer expressions into grouped logical conditions.

Expressions and Placeholders

Expr(sql, args...) lets you write a raw SQL fragment with bound args. If one of the args is itself a Sqlizer, Expr inlines that nested SQL and appends its args instead of binding the nested value as a single argument.

sql, args, err := nql.Expr(
    "COALESCE(?, ?)",
    nql.Expr("nickname"),
    "unknown",
).ToSql()

Result:

COALESCE(nickname, ?)

Arguments:

[]any{"unknown"}

A double question mark is treated as an escaped literal question mark during placeholder rewriting. This is useful when the SQL text itself needs a ? token. At the Expr stage, escaped ?? stays in the SQL text:

sql, args, err := nql.Expr("count(??)", nql.Expr("x")).ToSql()

Result:

count(??)

Arguments:

[]any{nql.Expr("x")}

Placeholder conversion happens only at the outer builder level. The default format is Question, and the package also provides Dollar, Colon, and AtP. Nested builders are rendered in raw form first, then placeholders are rewritten once by the outer builder. During that final rewrite, escaped ?? collapses to a literal ? instead of becoming a numbered placeholder.

If your application consistently uses one placeholder style, configure it once at startup and new builders will pick it up automatically:

func main() {
    nql.SetDefaultPlaceholderFormat(nql.Dollar)
}

Per-builder PlaceholderFormat(...) still overrides the package default when a specific query needs a different placeholder style.

Important limitations

  • The package does not validate SQL identifiers or protect from malformed raw fragments passed as strings.
  • Eq and NotEq reject list values; use In and NotIn instead.
  • Like and comparison helpers reject nil and list values.
  • Builders return errors when required parts are missing, for example no SELECT columns, no INSERT values, or no UPDATE SET clause.
  • MustSql() is available for convenience, but it panics on invalid builder state.

When to use nql

nql fits projects that need a small SQL builder without the weight of a full ORM. It is suitable when queries should remain explicit SQL, but you still want safe argument binding, composable subqueries, stable output, and reusable query fragments in plain Go code.

Benchmarks

Benchmarks in this repository reflect the goal of keeping a query-builder style similar to squirrel while avoiding reflection and query-execution wrappers. On darwin/arm64 with Apple M3, go test -bench . -benchmem produced the following results for equivalent query-building scenarios:

Scenario squirrel ns/op nql ns/op squirrel B/op nql B/op squirrel allocs/op nql allocs/op
SelectSimple 1779 350.3 2440 752 42 15
SelectComplex 8857 1764 11157 3865 205 67
InsertMultiRow 3123 788.7 4041 1504 80 31
UpdateWithCase 3893 1022 5362 2288 102 41
DeleteSimple 1340 157.3 1579 275 31 8
PlaceholderRewriteDollar 3215 667.4 4131 1232 78 28

These numbers are environment-specific, but they show the current baseline for the reflection-free implementation in this repository against the same benchmark scenarios implemented with squirrel.

About

nql is a lightweight, reflection-free SQL builder for Go that generates SQL and bound args without executing queries.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages