Skip to content

bawdo/gosbee

Repository files navigation

gosbee

CI codecov Go Report Card GoDoc

gosbee with plugins

gosbee is a Go SQL Builder — a powerful SQL AST (Abstract Syntax Tree) library inspired by Ruby's Arel.

Build SQL queries programmatically using composable, type-safe Go code. Instead of concatenating strings, you construct a tree of nodes that is only converted to SQL at the last moment by a database-specific visitor. This gives you semantic understanding of your queries, dialect-agnostic query building, and the ability to transform queries through middleware before SQL generation.

Features

  • 🌳 AST-based query building — queries are trees, not strings
  • 🗄️ Multi-dialect support — PostgreSQL, MySQL, SQLite via the Visitor pattern
  • 🔗 Composable — subqueries, complex JOINs, CTEs, and set operations
  • 🔌 Plugin system — transform the AST with middleware (access control, soft-delete, multi-tenancy)
  • Late binding — SQL is only generated when you call ToSQL()
  • 🛡️ Parameterised queries — built-in SQL injection protection
  • 💻 Interactive REPL — explore and test queries against live databases
  • 100% Ruby Arel feature parity — all core SQL features supported

Quick Start

Installation

go get github.com/bawdo/gosbee

Import Styles

gosbee supports two import styles to suit different preferences:

Simple imports (Recommended for most users)

Use the convenience package for a cleaner import and shorter function names:

import "github.com/bawdo/gosbee"

users := gosbee.NewTable("users")
query := gosbee.NewSelect(users).
    Select(users.Col("id"), users.Col("name")).
    Where(users.Col("active").Eq(gosbee.BindParam(true)))

visitor := gosbee.NewPostgresVisitor()
sql, params, _ := query.ToSQL(visitor)

Explicit imports (Advanced usage)

Import subpackages directly for full control and access to advanced features:

import (
    "github.com/bawdo/gosbee/managers"
    "github.com/bawdo/gosbee/nodes"
    "github.com/bawdo/gosbee/visitors"
)

users := nodes.NewTable("users")
query := managers.NewSelectManager(users).
    Select(users.Col("id"), users.Col("name")).
    Where(users.Col("active").Eq(nodes.BindParam(true)))

visitor := visitors.NewPostgresVisitor()
sql, params, _ := query.ToSQL(visitor)

You can also mix both approaches — use the convenience package for common operations and import subpackages for advanced features like window functions or custom node types.

Basic Usage

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/bawdo/gosbee"
    "github.com/jackc/pgx/v5"
)

func main() {
    // Connect to PostgreSQL
    conn, err := pgx.Connect(context.Background(), "postgres://user:pass@localhost/dbname")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(context.Background())

    // Define tables
    users := gosbee.NewTable("users")
    posts := gosbee.NewTable("posts")

    // Build a query with parameterised values
    query := gosbee.NewSelect(users).
        Select(users.Col("id"), users.Col("name"), posts.Col("title")).
        Join(posts).On(users.Col("id").Eq(posts.Col("user_id"))).
        Where(users.Col("active").Eq(gosbee.BindParam(true))).
        Order(posts.Col("created_at").Desc()).
        Limit(gosbee.BindParam(10))

    // Generate SQL for PostgreSQL (params enabled by default)
    visitor := gosbee.NewPostgresVisitor()
    sql, params, err := query.ToSQL(visitor)
    if err != nil {
        panic(err)
    }

    fmt.Println(sql)
    // SELECT "users"."id", "users"."name", "posts"."title"
    // FROM "users"
    // INNER JOIN "posts" ON "users"."id" = "posts"."user_id"
    // WHERE "users"."active" = $1
    // ORDER BY "posts"."created_at" DESC
    // LIMIT $2
    fmt.Println(params) // []any{true, 10}

    // Execute the query with parameters (safe from SQL injection)
    rows, err := conn.Query(context.Background(), sql, params...)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    // Process results
    for rows.Next() {
        var id int
        var name, title string
        if err := rows.Scan(&id, &name, &title); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("User %d: %s - Post: %s\n", id, name, title)
    }
}

Parameterised Queries

Protect against SQL injection with parameterised queries:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/bawdo/gosbee"
    "github.com/jackc/pgx/v5"
)

func main() {
    // Connect to PostgreSQL
    conn, err := pgx.Connect(context.Background(), "postgres://user:pass@localhost/dbname")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(context.Background())

    users := gosbee.NewTable("users")

    // Build parameterised query using BindParam
    query := gosbee.NewSelect(users).
        Select(users.Col("id"), users.Col("name"), users.Col("age")).
        Where(users.Col("name").Eq(gosbee.BindParam("Alice"))).
        Where(users.Col("age").Gt(gosbee.BindParam(18)))

    // Parameterisation is enabled by default
    visitor := gosbee.NewPostgresVisitor()
    sql, params, err := query.ToSQL(visitor)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(sql)
    // SELECT "users"."id", "users"."name", "users"."age"
    // FROM "users"
    // WHERE "users"."name" = $1 AND "users"."age" > $2
    fmt.Println(params) // []any{"Alice", 18}

    // Execute with parameters (safe from SQL injection)
    rows, err := conn.Query(context.Background(), sql, params...)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    // Process results
    for rows.Next() {
        var id, age int
        var name string
        if err := rows.Scan(&id, &name, &age); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("User %d: %s (age %d)\n", id, name, age)
    }
}

Disabling Parameterisation (Not Recommended)

⚠️ WARNING: Disabling parameterisation removes SQL injection protection. Only use for debugging or when all values are trusted. Production code should NEVER use this option.

// Disable parameterisation (literals instead of placeholders)
visitor := gosbee.NewPostgresVisitor(gosbee.WithoutParams())
sql, _, err := query.ToSQL(visitor)
// SELECT "users"."id", "users"."name", "users"."age"
// FROM "users"
// WHERE "users"."name" = 'Alice' AND "users"."age" > 18
// (Note: No $1, $2 placeholders - values are inlined)

Using Plugins

Transform queries with plugins before SQL generation:

import (
    "github.com/bawdo/gosbee"
    "github.com/bawdo/gosbee/plugins/softdelete"
)

users := gosbee.NewTable("users")
query := gosbee.NewSelect(users).
    Select(users.Star()).
    Use(softdelete.New())

visitor := gosbee.NewPostgresVisitor()
sql, _, _ := query.ToSQL(visitor)
// SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL

Architecture

gosbee follows a layered architecture inspired by Ruby's Arel:

Layer Package Purpose
Nodes nodes/ AST building blocks — tables, attributes, predicates, literals
Managers managers/ Fluent DSL for composing queries (SELECT, INSERT, UPDATE, DELETE)
Visitors visitors/ Render the AST into dialect-specific SQL
Plugins plugins/ Transform the AST before SQL generation (optional)
Manager → [Plugin 1] → [Plugin 2] → ... → Visitor → SQL string
           Transform    Transform         Accept

This architecture allows you to:

  • Build queries once, render for different databases
  • Transform queries with middleware (access control, logging, etc.)
  • Compose complex queries from smaller pieces
  • Introspect and manipulate queries programmatically

Supported SQL Features

gosbee supports the full range of modern/ANSI SQL features:

Queries

  • SELECT, INSERT, UPDATE, DELETE
  • Projections (SELECT columns)
  • WHERE conditions with predicates (=, !=, >, <, LIKE, IN, BETWEEN, etc.)
  • JOINs (INNER, LEFT/RIGHT/FULL OUTER, CROSS, LATERAL)
  • GROUP BY / HAVING
  • ORDER BY with NULLS FIRST/LAST
  • LIMIT / OFFSET
  • DISTINCT / DISTINCT ON

Advanced Features

  • Window functions (ROW_NUMBER, RANK, LAG, LEAD, etc.) with frames
  • Common Table Expressions (WITH / WITH RECURSIVE)
  • Set operations (UNION, INTERSECT, EXCEPT)
  • Subqueries and table aliases
  • Aggregate functions (COUNT, SUM, AVG, MIN, MAX)
  • Named functions (COALESCE, CAST, LOWER, UPPER, etc.)
  • CASE expressions (searched and simple)
  • Advanced grouping (CUBE, ROLLUP, GROUPING SETS)
  • EXISTS / NOT EXISTS
  • Query comments and optimizer hints
  • Locking clauses (FOR UPDATE, FOR SHARE, SKIP LOCKED)

DML Operations

  • Multi-row INSERT
  • INSERT FROM SELECT
  • UPSERT (ON CONFLICT DO NOTHING / DO UPDATE)
  • RETURNING clause (PostgreSQL, SQLite)

SQL Dialects

Built-in support for three major databases:

Dialect Visitor Identifier Quoting Placeholders
PostgreSQL NewPostgresVisitor() "table"."column" $1, $2, $3
MySQL NewMySQLVisitor() `table`.`column` ?, ?, ?
SQLite NewSQLiteVisitor() "table"."column" ?, ?, ?

Dialect-specific features (DISTINCT ON, LATERAL JOIN, RETURNING, etc.) are handled automatically by the visitors.

See the Visitor Dialects guide for details.

Interactive REPL

gosbee includes an interactive shell for exploring queries:

# Install
go install github.com/bawdo/gosbee/cmd/repl@latest

# Run
export GOSBEE_ENGINE=postgres
export DATABASE_URL="postgres://user:pass@localhost/mydb"
repl

The REPL provides:

  • Tab completion for tables and columns
  • Live query execution
  • Plugin support
  • DOT/Graphviz visualisation
  • Expression evaluation
  • Command history

See the REPL guide for the full feature set.

Documentation

For Library Users

For Contributors

Plugin Documentation

Acknowledgements

This project is heavily inspired by Ruby's Arel library, which pioneered the SQL AST builder pattern. Arel was created by Bryan Helmkamp and has been the foundation of ActiveRecord's query interface since Rails 3.0.

gosbee aims to bring Arel's elegant design to the Go ecosystem with:

  • Idiomatic Go patterns and conventions
  • Strong typing throughout
  • Zero external dependencies in the core library
  • Extended plugin architecture for middleware

We're grateful to the Arel maintainers and the Ruby community for proving out this approach to SQL query building.

Project Status

gosbee has achieved 100% feature parity with Ruby Arel's core SQL features. The library is under active development with ~1096 tests covering all major functionality.

Current focus areas:

  • Performance optimisation
  • Additional dialect support
  • Production-ready plugin implementations
  • Extended REPL features

Contributing

We welcome contributions! Whether you're fixing a bug, improving documentation, or adding a new feature, please see our Contributing Guide for guidelines.

Common ways to contribute:

  • 🐛 Report bugs or suggest features via issues
  • 📖 Improve documentation and examples
  • 🔌 Write new plugins for common use cases
  • 🗄️ Add support for additional SQL dialects
  • ✅ Add tests for edge cases

License

This project is licensed under the MIT License - see the LICENSE file for details.

Copyright (c) 2026 Keith Bawden

Related Projects

  • Arel — The Ruby library that inspired this project
  • squirrel — Another Go SQL builder (string-based, not AST-based)
  • goqu — SQL builder with dialect support

Links

About

A Golang SQL Builder

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •