Skip to content
GlshchnkLxPublic

About

Type-safe SQL control layer for Go. Generic Table[T], expression builder, FK auto-join, and safe-by-default migrations for SQLite, MySQL, and Postgres

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-sqlkit

Go License

Type-safe SQL control layer for Go 1.25+. Generic Table[T], composable expression builder, explicit JOINs with FK auto-join, composite indexes, and safe-by-default migrations — for mysql, sqlite, and postgres.

No interface{}/reflect in return types. Full parameterization through a shared expr.Writer (continuous placeholder counter). Savepoint-based nested transactions. ~10% overhead vs hand-written db.Query + Scan.

Quick Start

package main

import (
    "context"
    "log"

    sqlkit "github.com/GlshchnkLx/go-sqlkit"
    _ "modernc.org/sqlite"
)

type User struct {
    ID   int64  `sql:"id,pk,autoincr"`
    Name string `sql:"name"`
    Age  int    `sql:"age"`
}

func main() {
    db, err := sqlkit.Open("sqlite", ":memory:", "sqlite")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    ctx := context.Background()

    users, _ := sqlkit.Register[User](db, sqlkit.WithTableName("users"))

    create, _ := sqlkit.NewCreate[User](sqlkit.WithTableName("users"))
    db.Exec(ctx, create.WithIfNotExists())

    ins, _ := sqlkit.NewInsert[User](sqlkit.WithTableName("users"))
    db.Exec(ctx, ins.Rows(&User{Name: "Alice", Age: 30}))

    got, _ := sqlkit.Query[User](db).OrderBy(users.Field("id")).First(ctx)
    log.Printf("user: %+v", got)
}

Installation

go get github.com/GlshchnkLx/go-sqlkit

Import drivers for side effects:

import (
    _ "github.com/go-sql-driver/mysql"  // mysql
    _ "github.com/lib/pq"               // postgres
    _ "modernc.org/sqlite"              // sqlite (pure Go, no cgo)
)

Features

sql struct tags — define table metadata inline:

type Membership struct {
    TenantID int64  `sql:"tenant_id,pk,index=by_tenant_role"`
    UserID   int64  `sql:"user_id,pk,index=by_tenant_role"`
    Role     string `sql:"role"`
}

Tags support: pk, autoincr, notnull, unique/unique=group, index/index=group, type=, default=, check=, fk=table.col, name=, old>new (rename detection).

Table options: WithTableName, WithPrimaryKey, WithIndex, WithIndexMethod, WithUnique, WithIndexPrefix, WithUniquePrefix.

Database options: WithDatabaseIndexPrefix, WithDatabaseUniquePrefix, WithDatabaseScanCacheSize.

Expression builder — composable WHERE/SET predicates:

sqlkit.Query[User](db).
    Where(expr.And(
        expr.Gt(users.Field("age"), 18),
        expr.Or(
            expr.Like(users.Field("name"), "A%"),
            expr.Eq(users.Field("role"), "admin"),
        ),
    )).
    All(ctx)

Includes Eq/Ne/Gt/Lt/Ge/Le, Like/NotLike, In/NotIn, Between, IsNull, Regexp, And/Or/Not, Match (&struct → AND equalities), RawExpr, subqueries (Exists/Any/All/Subquery).

Typed DML/DDL builders:

Builder Features
Create[T] WithIfNotExists, IndexStatements()
Insert[T] Multi-row, upsert (OnConflict), Returning (pg)
Update[T] Set, Where/WhereAnd/WhereOr, All(), Returning (pg)
Delete[T] Where/WhereAnd/WhereOr, All(), Returning (pg)
Select From/FromSubquery, Join, GroupBy/Having, OrderBy, Limit/Offset, ForUpdate/ForShare, Union/Intersect/Except, Distinct
Replace[T] MySQL REPLACE INTO, SQLite INSERT OR REPLACE

Auto-join — embed registered structs, auto-resolve FK JOINs:

type UserWithProfile struct {
    User
    Profile
}
rows, _ := sqlkit.Query[UserWithProfile](db).FromAuto().All(ctx)

Many-to-many via Through:

sqlkit.Query[PostWithTags](db).
    FromAuto().
    Through("post_tags", "post_id", "tag_id").
    All(ctx)

Safe-by-default migrations — auto-diff applies safe changes (add table, add nullable column, add/drop index/unique) and refuses risky ones (drop, type change, PK change) — those require a file migration:

db.RegisterMigration(migrate.File{
    Version: 1,
    Up:   func(ctx context.Context, tx *sql.Tx) error { /* ... */ },
    Down: func(ctx context.Context, tx *sql.Tx) error { /* ... */ },
})
res, err := db.Migrate(ctx) // files + auto-diff in one transaction

Nested transactions via savepoints:

tx, _ := db.Begin(ctx)
nested, _ := tx.Begin(ctx) // SAVEPOINT
nested.Rollback(ctx)       // ROLLBACK TO SAVEPOINT — outer survives
tx.Commit(ctx)

Dialects:

SQLite MySQL Postgres
Placeholder ? ? $1, $2, …
Quote "…" `…` "…"
Autoincrement AUTOINCREMENT AUTO_INCREMENT GENERATED BY DEFAULT AS IDENTITY
time.Time → TEXT DATETIME TIMESTAMPTZ
[]byte → BLOB BLOB BYTEA
[16]byte (UUID) → BLOB BINARY(16) UUID
REPLACE INSERT OR REPLACE REPLACE INTO ❌
RETURNING ❌ ❌ ✅

Custom type mapping: dialect.RegisterCustomType(reflect.TypeFor[MyUUID](), "UUID")

Cached scan plan — LRU cache keyed by (T, columns), typed buffers, no reflection per row.

Errors

Sentinel errors checked via errors.Is:

Sentinel When
ErrTableNotFound Table not registered
ErrTableInvalid Invalid table definition
ErrNoRows One/First returned 0 rows
ErrMoreThanRequested One returned >1 row
ErrBuilderInvalid Misconfigured builder (empty SET, no FROM, etc.)
ErrTransportNotSupported Operation needs *sql.DB, Transport doesn't provide it
ErrConflictingOption Conflicting migration options

Typed errors via errors.As: *ErrTableInvalidError (Field/Opt/Reason/Cause), *ErrMigrationRequiresFile (Change/Hint).

Examples

Run with go run -tags examples ./examples/<name>:

Example Demonstrates
basic Open, Register, Create, Insert, Query
expr Eq/And/Or, Gt/Like/In, Match, RawExpr
join Explicit JOIN, FK auto-join, many-to-many via Through
composite Composite PK + index (tags and WithIndex)
migrate Auto-diff + file migration up/down + dry-run
transaction Nested savepoints
postgres $N placeholders, IDENTITY (env-gated)
upsert OnConflictDoNothing / OnConflictDoUpdate
union Union / UnionAll / Intersect / Except
returning Postgres RETURNING on Insert/Update/Delete
lock ForUpdate / ForShare / NOWAIT / SKIP LOCKED
custom-type RegisterCustomType + UUID + json.RawMessage
demo Full CLI todo-list app

Testing

go test ./...           # in-memory SQLite
go test -race ./...     # with race detector
go test -cover ./...    # coverage report
go vet ./...            # static analysis
go test -bench=. ./...  # benchmarks

MySQL/Postgres via env vars: SQLKIT_TEST_MYSQL_DSN=... / SQLKIT_TEST_POSTGRES_DSN=...

Coverage by package: sqlkit 92.8%, dialect 81.6%, table 90.2%, query 45.0%, expr 46.8%, migrate 51.4%.

Documentation

Detailed docs in docs/: architecture, testing, error handling, file migrations, dialect/expr/migrate packages, mental map.

License

Apache License 2.0 — see LICENSE.

About

Type-safe SQL control layer for Go. Generic Table[T], expression builder, FK auto-join, and safe-by-default migrations for SQLite, MySQL, and Postgres

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages