qqm

package module
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 8 Imported by: 0

README

qqm — Quick Query Maker

qqm — это ORM-подобная Go-библиотека для типизированной работы с SQL-базами данных через структуры. Она автоматически генерирует SQL-запросы на основе тегов в полях структуры и предоставляет простой CRUD-интерфейс, включая multi-table SELECT с JOIN.

Возможности

  • Типизированные таблицыTable[ROW] параметризуется вашей структурой.
  • Multi-table запросыQuery[QROW] для SELECT с JOIN по ref-связям.
  • Автогенерация SQL — INSERT, UPDATE, SELECT, DELETE строятся по метаданным структуры.
  • Поддержка диалектов — SQLite (?) и PostgreSQL ($1, $2, …).
  • CRUD-интерфейс — Insert, Update, GetByKey, Delete, List.
  • Гибкая фильтрация — And/Or-комбинации, операторы Eq, Gt, Lt, Gte, Lte, Between, In.
  • Квалифицированные имена — фильтры по полям присоединённых таблиц ("Order.Amount").
  • LEFT JOIN с nil*ROW-поля автоматически становятся nil при отсутствии строки.
  • Теги полей — колонка, первичный ключ, внешний ключ, readonly, auto, omit, join, on, table, primary.
  • Embedded структуры — поддержка встраивания с префиксом колонок.
  • Именованные поля-структуры — префикс для неанонимных структур (например, несколько адресов).
  • Составные ключи —переменное количество полей в PK.
  • Кеширование SQL — запросы генерируются один раз при первом обращении.
  • Без рефлексии в рантайме — метаданные собираются лениво и кешируются.

Установка

go get github.com/mirrorru/qqm

Быстрый старт

Определение модели
type User struct {
    ID    int64  `qqm:"pk"`
    Name  string
    Email string
    Age   int
}

Правила именования по умолчанию:

  • Имя таблицы — snake_case от имени структуры: user.
  • Имя колонки — snake_case от имени поля: name, email, age.
Создание таблицы и SQL
import "github.com/mirrorru/qqm"

userTable := qqm.NewTable[User](qqm.SQLiteDialect)

fmt.Println(userTable.Internals().InsertSQL())
// INSERT INTO user (id, name, email, age) VALUES (?, ?, ?, ?) RETURNING id, name, email, age

fmt.Println(userTable.Internals().SelectSQL())
// SELECT id, name, email, age FROM user WHERE id = ?
Полный CRUD
import (
    "context"
    "database/sql"
    "github.com/mirrorru/qqm"
    "github.com/mirrorru/qqm/executor"
)

func Example() {
    db, _ := sql.Open("sqlite", ":memory:")
    db.Exec(`CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT, email TEXT, age INTEGER
    )`)

    ex := executor.NewDBAdapter(db)
    ctx := context.Background()
    tbl := qqm.NewTable[User](qqm.SQLiteDialect)

    // Create
    u, _ := tbl.Insert(ctx, ex, &User{Name: "Alice", Email: "alice@test.com", Age: 25})

    // Read
    alice, _ := tbl.GetByKey(ctx, ex, u.ID)

    // Update
    alice.Age = 26
    tbl.Update(ctx, ex, alice)

    // List with filter
    result, _ := tbl.List(ctx, ex, qqm.Field("Age", qqm.And, qqm.Gt(20)))

    // Delete
    tbl.Delete(ctx, ex, u.ID)
}

Настройка колонок через теги

Формат тега: qqm:"col=name;pk;ref=table.col;readonly;auto;omit;prefix=...;join=TYPE;on=...;table=...;primary"

Опция Описание
col=name Имя колонки в БД (по умолчанию: snake_case от имени поля)
pk Поле является первичным ключом
ref=table.col Внешний ключ
prefix=... Префикс для колонок из embedded или именованной структуры
readonly Не участвует в UPDATE
auto Не участвует в INSERT (например, SERIAL)
omit Полностью исключается из SQL
join=TYPE Тип JOIN для Query: LEFT, INNER, RIGHT, FULL
on=... Явное условие JOIN (переопределяет авто-вывод из ref=)
table=... Переопределение имени таблицы для Query-поля
primary Явное указание primary-таблицы в Query
Префикс для именованных полей-структур

Тег prefix работает не только для embedded (анонимных) структур, но и для именованных полей-структур. Это позволяет переиспользовать одну и ту же Go-структуру для разных табличных колонок:

type Address struct {
    City   string
    Street string
    Zip    string
}

type Person struct {
    ID          int64   `qqm:"pk"`
    Name        string
    HomeAddress Address `qqm:"prefix=home_"`
    WorkAddress Address `qqm:"prefix=work_"`
}
// Колонки: id, name, home_city, home_street, home_zip, work_city, work_street, work_zip

Multi-table запросы (JOIN)

Query[QROW] — типизированный SELECT с JOIN. Поля QROW — существующие ROW-структуры. JOIN-условия выводятся автоматически из тегов ref=.

Определение Query-структуры
// ROW-структуры
type User struct {
    ID    int64  `qqm:"pk"`
    Name  string
    Email string
}

type Order struct {
    ID     int64   `qqm:"pk;auto"`
    UserID int64   `qqm:"ref=users.id"`
    Amount float64
}

// Query-структура
type UserWithOrder struct {
    User  User    // INNER JOIN
    Order *Order  // LEFT JOIN (указатель → nil при отсутствии строки)
}
Использование
q, err := qqm.NewQuery[UserWithOrder](qqm.SQLiteDialect)
// err != nil если не найден FK для JOIN

results, err := q.List(ctx, ex,
    qqm.AndFilter(
        qqm.Field("User.Name", qqm.And, qqm.Eq("Alice")),
        qqm.Field("Order.Amount", qqm.And, qqm.Gt(100.0)),
    ),
)

for _, r := range results {
    fmt.Println(r.User.Name, r.Order) // Order == nil для LEFT JOIN без строки
}
Правила вывода JOIN
Тип поля JOIN по умолчанию
Order (value) INNER
*Order (pointer) LEFT

JOIN-условие ON строится по тегу ref=users.id на поле UserID структуры Order: orders.user_id = users.id.

Явное управление JOIN
type CustomQuery struct {
    User  User   `qqm:"table=app_users;primary"`  // переопределение имени + primary
    Order *Order `qqm:"join=LEFT"`                 // явный тип JOIN
}

type WithExplicitON struct {
    User User
    Ref  RefData `qqm:"on=ref.user_id=users.id"`   // явное условие
}
LEFT JOIN и nil

Если строки в присоединённой таблице нет, поле-указатель устанавливается в nil:

type UserWithOrderPtr struct {
    User  User
    Order *Order
}

results, _ := q.List(ctx, ex)
for _, r := range results {
    if r.Order == nil {
        fmt.Println(r.User.Name, "has no orders")
    }
}
Фильтры с квалифицированными именами

Имена полей в фильтрах: "TableName.FieldName":

qqm.AndFilter(
    qqm.Field("User.Name", qqm.And, qqm.Eq("Alice")),
    qqm.Field("Order.Amount", qqm.And, qqm.Gt(200.0)),
)

Примеры

Составной ключ
type OrgUser struct {
    OrgID  int64  `qqm:"pk"`
    UserID int64  `qqm:"pk"`
    Role   string
}
Кастомное имя таблицы

Реализуйте интерфейс table.SQLNamer:

func (u *OrgUser) SQLName() string { return "org_members" }
Embedded структуры с префиксом
type Audit struct {
    CreatedAt int64 `qqm:"col=created_at"`
    UpdatedAt int64 `qqm:"col=updated_at"`
}

type Post struct {
    ID    int64 `qqm:"pk"`
    Title string
    Audit `qqm:"prefix=audit_"`
}
// Колонки: id, title, audit_created_at, audit_updated_at
// AND-условия
andFilter := qqm.AndFilter(
    qqm.Field("Age", qqm.And, qqm.Gte(18), qqm.Lte(60)),
    qqm.Field("Status", qqm.And, qqm.Eq("active")),
)

// OR-условия
orFilter := qqm.OrFilter(
    qqm.Field("Role", qqm.And, qqm.Eq("admin")),
    qqm.Field("Role", qqm.And, qqm.Eq("moderator")),
)

// Between
ageFilter := qqm.AndFilter(
    qqm.Field("Age", qqm.And, qqm.Between(18, 65)),
)

// In
nameFilter := qqm.AndFilter(
    qqm.Field("Name", qqm.And, qqm.In("Alice", "Bob", "Charlie")),
)

Диалекты

Диалект Плейсхолдер RETURNING
qqm.SQLiteDialect ? Да
qqm.PostgreSQLDialect $1, $2, … Да

Адаптеры БД

Для передачи в CRUD-методы используйте адаптеры из executor:

Адаптер Конструктор Для чего
executor.DBAdapter executor.NewDBAdapter(db) *sql.DB
executor.TxAdapter executor.NewTxAdapter(tx) *sql.Tx
executor.PGXAdapter executor.NewPGXAdapter(conn) *pgx.Conn
executor.PGXTxAdapter executor.NewPGXTxAdapter(tx) pgx.Tx
Транзакции
tx, _ := db.BeginTx(ctx, nil)
ex := executor.NewTxAdapter(tx)

inserted, err := tbl.Insert(ctx, ex, &User{Name: "Alice"})
if err != nil {
    _ = tx.Rollback()
    return err
}
_ = tx.Commit()

Все CRUD-методы (Insert, Update, GetByKey, Delete, List) работают как с DBAdapter, так и с TxAdapter.

Интерфейс Executor

Пакет executor определяет интерфейс для абстракции SQL-выполнения:

type Executor interface {
    ExecContext(ctx context.Context, query string, args ...any) (Result, error)
    QueryContext(ctx context.Context, query string, args ...any) (Rows, error)
    QueryRowContext(ctx context.Context, query string, args ...any) Row
}
  • QueryRowContext — для запросов, возвращающих одну строку (Insert RETURNING, GetByKey).

Documentation

Overview

Example (CompositeKey)

Example_compositeKey demonstrates usage with a composite key

// Created at 2026-06-28
package main

import (
	"fmt"

	"github.com/mirrorru/qqm"
	"github.com/mirrorru/qqm/dialect"
)

// OrgUser — структура с составным ключом
type OrgUser struct {
	OrgID  int64 `qqm:"pk"`
	UserID int64 `qqm:"pk"`
	Name   string
	Email  string
}

func (o *OrgUser) SQLName() string { return "org_users" }

// Example_compositeKey demonstrates usage with a composite key
func main() {
	orgUserTable := qqm.NewTable[OrgUser](dialect.SQLiteDialect{})

	fmt.Println("INSERT:", orgUserTable.Internals().InsertSQL())
	fmt.Println("UPDATE:", orgUserTable.Internals().UpdateSQL())
	fmt.Println("SELECT:", orgUserTable.Internals().SelectSQL())
	fmt.Println("DELETE:", orgUserTable.Internals().DeleteSQL())

	meta := orgUserTable.Internals().Meta()
	fmt.Println("Table:", meta.TableName)
	fmt.Println("Columns:", meta.Columns)
	fmt.Println("PK count:", len(meta.PKFields))
	for _, pk := range meta.PKFields {
		fmt.Printf("  PK: %s (order: %d)\n", pk.Column, pk.PkOrder)
	}

}
Output:
INSERT: INSERT INTO org_users (org_id, user_id, name, email) VALUES (?, ?, ?, ?) RETURNING org_id, user_id, name, email
UPDATE: UPDATE org_users SET name = ?, email = ? WHERE org_id = ? AND user_id = ?
SELECT: SELECT org_id, user_id, name, email FROM org_users WHERE org_id = ? AND user_id = ?
DELETE: DELETE FROM org_users WHERE org_id = ? AND user_id = ?
Table: org_users
Columns: [org_id user_id name email]
PK count: 2
  PK: org_id (order: 1)
  PK: user_id (order: 2)
Example (SimpleKey)

Example_simpleKey demonstrates usage with a simple key

// Created at 2026-06-28
package main

import (
	"fmt"

	"github.com/mirrorru/qqm"
	"github.com/mirrorru/qqm/dialect"
)

// User — структура с простым ключом
type User struct {
	ID    int64 `qqm:"pk"`
	Name  string
	Email string
	Age   int
}

// Example_simpleKey demonstrates usage with a simple key
func main() {
	userTable := qqm.NewTable[User](dialect.SQLiteDialect{})

	fmt.Println("INSERT:", userTable.Internals().InsertSQL())
	fmt.Println("UPDATE:", userTable.Internals().UpdateSQL())
	fmt.Println("SELECT:", userTable.Internals().SelectSQL())
	fmt.Println("DELETE:", userTable.Internals().DeleteSQL())

	meta := userTable.Internals().Meta()
	fmt.Println("Table:", meta.TableName)
	fmt.Println("Columns:", meta.Columns)

}
Output:
INSERT: INSERT INTO user (id, name, email, age) VALUES (?, ?, ?, ?) RETURNING id, name, email, age
UPDATE: UPDATE user SET name = ?, email = ?, age = ? WHERE id = ?
SELECT: SELECT id, name, email, age FROM user WHERE id = ?
DELETE: DELETE FROM user WHERE id = ?
Table: user
Columns: [id name email age]

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	SQLiteDialect     = dialect.SQLiteDialect{}
	PostgreSQLDialect = dialect.PostgreSQLDialect{}
)

Functions

This section is empty.

Types

type CRUD added in v0.1.4

type CRUD[ROW any] interface {
	Internals() *tableInternals

	Insert(ctx context.Context, ex executor.Executor, src *ROW) (*ROW, error)
	Update(ctx context.Context, ex executor.Executor, src *ROW) error
	GetByPK(ctx context.Context, ex executor.Executor, keys ...any) (*ROW, error)
	Delete(ctx context.Context, ex executor.Executor, keys ...any) error
	List(ctx context.Context, ex executor.Executor, filters ...Filter) ([]*ROW, error)
}

type Condition

type Condition struct {
	Op    ConditionOp
	Value any
}

func Between

func Between(min, max any) Condition

func Eq

func Eq(value any) Condition

func Gt

func Gt(value any) Condition

func Gte

func Gte(value any) Condition

func In

func In(values ...any) Condition

func Lt

func Lt(value any) Condition

func Lte

func Lte(value any) Condition

type ConditionOp

type ConditionOp int
const (
	OpEq ConditionOp = iota
	OpGt
	OpLt
	OpGte
	OpLte
	OpBetween
	OpIn
)

type FieldFilter

type FieldFilter struct {
	Field      string
	Conditions []Condition
	Op         FilterOp
}

func Field

func Field(fieldName string, op FilterOp, conditions ...Condition) FieldFilter

type Filter

type Filter struct {
	Fields []FieldFilter
	Op     FilterOp
}

func AndFilter

func AndFilter(fields ...FieldFilter) Filter

func OrFilter

func OrFilter(fields ...FieldFilter) Filter

type FilterOp

type FilterOp int
const (
	And FilterOp = iota
	Or
)

type Query added in v0.1.4

type Query[QROW any] struct {
	// contains filtered or unexported fields
}

func NewQuery added in v0.1.2

func NewQuery[QROW any](d dialect.DialectProvider) (*Query[QROW], error)

func (*Query[QROW]) List added in v0.1.4

func (q *Query[QROW]) List(ctx context.Context, ex executor.Executor, filters ...Filter) ([]*QROW, error)

type SQLNamer added in v0.1.4

type SQLNamer interface {
	SQLName() string
}

type Table added in v0.1.4

type Table[ROW any] struct {
	// contains filtered or unexported fields
}

func NewTable

func NewTable[ROW any](d dialect.DialectProvider) *Table[ROW]

func (*Table[ROW]) Delete added in v0.1.4

func (t *Table[ROW]) Delete(ctx context.Context, ex executor.Executor, keys ...any) error

func (*Table[ROW]) GetByPK added in v0.1.4

func (t *Table[ROW]) GetByPK(ctx context.Context, ex executor.Executor, keys ...any) (*ROW, error)

func (*Table[ROW]) Insert added in v0.1.4

func (t *Table[ROW]) Insert(ctx context.Context, ex executor.Executor, src *ROW) (*ROW, error)

func (*Table[ROW]) Internals added in v0.1.4

func (t *Table[ROW]) Internals() *tableInternals

func (*Table[ROW]) List added in v0.1.4

func (t *Table[ROW]) List(ctx context.Context, ex executor.Executor, filters ...Filter) ([]*ROW, error)

func (*Table[ROW]) Update added in v0.1.4

func (t *Table[ROW]) Update(ctx context.Context, ex executor.Executor, src *ROW) error

Directories

Path Synopsis
Created at 2026-06-28
Created at 2026-06-28
Created at 2026-06-28
Created at 2026-06-28
Updated at 2026-06-29
Updated at 2026-06-29
test
fixtures
Created at 2026-06-28
Created at 2026-06-28

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL