---
title: "Организация кода: queries и models"
description: "Организация кода sqlc: раскладка репозитория, анатомия пакета, emit-ключи, пакеты queries, репозиторий поверх Queries и три движка в одном проекте."
course: "sqlc: типобезопасный код из SQL-запросов"
url: "https://golangmaster.ru/courses/sqlc/lessons/20-organizaciya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Организация кода: queries и models

*Курс «sqlc: типобезопасный код из SQL-запросов», урок 20 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/sqlc/lessons/20-organizaciya/*

Тема урока — организация кода sqlc: sqlc структура проекта, которая выдерживает не туториал на пять минут, а годы жизни репозитория. Второй урок собрал ваш первый sqlc.yaml, семнадцатый развёл домены по записям списка sql, восемнадцатый и девятнадцатый показали MySQL и SQLite рядом с PostgreSQL — и к этому моменту у трекера задач накопились вопросы, на которые «просто запусти sqlc generate» уже не отвечает. Куда класть файлы схемы и запросов, чтобы через полгода не пришлось искать? Из каких деталей складывается сгенерированный пакет — и можно ли договориться с генератором об их форме? Как делить запросы между файлами и пакетами так, чтобы каждый находил своё место? И главное — как жить с генератом ежедневно: оборачивать ли его в репозиторий, коммитить ли в git, и что делать, если в одном проекте три движка сразу. Урок отвечает на шесть вопросов. Первый: каноничная раскладка репозитория — где живут миграции, пакеты queries и сгенерированный код. Второй: анатомия пакета — db.go, models.go, querier.go и файлы-однофамильцы ваших .sql. Третий: emit-ключи и query_parameter_limit — рычаги, которыми форма генерата подгоняется под ваш стиль. Четвёртый: переиспользование — репозиторий поверх Queries, который не превращается во второй ORM. Пятый: три движка в одном репозитории без войны диалектов. Шестой: спор о генерате в git, который в двадцать втором уроке закроет CI. План курса — на [странице курса sqlc](/courses/sqlc/); разветвление на домены — [в семнадцатом уроке](/courses/sqlc/lessons/17-schemy/), а соседний рассказ про тесты — в двадцать первом.

### Репозиторий, в котором sqlc живёт долго

Туториал из третьего урока оставил вам плоскую папку: schema.sql, query.sql, один пакет. Для демо хватает, для продукта — нет: через полгода в одной папке встретятся схема на тысячу строк, сорок файлов запросов и попытка понять, что из этого актуально. Зрелая sqlc структура проекта опирается на три решения, и все три вы уже видели по кусочкам. Первое: схема живёт в папке миграций — [шестнадцатый урок](/courses/sqlc/lessons/16-migracii/) отвёл db/migrations под пары up/down у goose и golang-migrate, и sqlc читает эту же папку как источник DDL. Одна папка — один источник правды: запросы компилируются против того же SQL, который накатывается на базу. Второе: запросы живут отдельно, в пакетах queries — по домену на папку, как учил семнадцатый урок: db/queries/todos, db/queries/users. Третье: сгенерированный код уезжает в internal/store/<pkg> — не в корень, не в cmd, не рядом с обработчиками. Префикс internal работает на вас: за пределы модуля пакет импортировать нельзя, а значит, никто из чужих репозиториев случайно не начнёт зависеть от вашего генерата.

Отдельное слово про sqlc.yaml. По умолчанию команда ищет его в текущей папке, и для монорепозитория с несколькими сервисами это повод держать конфиг в корне — sqlc generate из корня соберёт все записи списка sql разом. Если сервисов много и генерация у каждого своя, конфиг можно положить рядом с сервисом и звать с флагом -f, но приберегите это для дня, когда одна запись действительно начнёт мешать другой; до того момента один файл в корне — самая читаемая организация кода sqlc, какая бывает.

### Канон урока: раскладка и конфиг трекера

```go
# sqlc.yaml — в корне репозитория трекера
version: "2"
sql:
  - engine: "postgresql"
    schema: "db/migrations"          # та же папка, что накатывает migrate
    queries:                          # список путей — не только один каталог
      - "db/queries/todos"
      - "db/queries/users"
    gen:
      go:
        package: "pgdb"
        out: "internal/store/pgdb"
        sql_package: "pgx/v5"
        emit_interface: true          # Querier — шов для тестов
        emit_json_tags: true
        emit_empty_slices: true

# Дерево репозитория:
# db/
#   migrations/        001_init.sql, 002_soft_delete.sql, ...
#   queries/
#     todos/queries.sql
#     users/queries.sql
# internal/
#   store/
#     pgdb/            <- сгенерировано, руками не трогаем
#     store.go         <- наш код: репозиторий поверх Queries
```

Три решения раскладки в одном файле. schema указывает на папку миграций — генератор прочтёт все .sql по порядку, как в шестнадцатом уроке. queries стал списком: каталоги доменов перечислены явно, и новый домен добавляется строкой, а не перекроем старой записи. out уводит генерат в internal/store/pgdb — под защиту internal. emit-ключи здесь три, полный их разбор — таблицей ниже.

### Анатомия пакета: четыре файла, из которых всё состоит

Откройте internal/store/pgdb после генерации — и вы увидите ровно четыре сорта файлов, какие бы таблицы ни завели. Файл db.go — каркас: интерфейс DBTX с методами QueryContext, QueryRowContext, ExecContext (для database/sql ещё и PrepareContext), структура Queries, хранящая соединение, конструктор New(db DBTX) и метод WithTx, который одиннадцатый урок делал главной дверью в транзакции. Этот файл одинаков почти дословно у всех контуров курса — меняются только сигнатуры под pgx или database/sql, как видел девятнадцатый.

Файл models.go — словарь: по структуре на таблицу, имена превращает простое правило — таблица todos даёт структуру Todo, срез хвостового «s» снимается; ключ emit_exact_table_names со значением true отключает это и оставит Authors для таблицы authors. Типы полей приходят из карты типов вашего движка — та самая, что читали четвёртый, восемнадцатый и девятнадцатый уроки.

Файлы-однофамильцы запросов: todos.sql.go рядом с вашим todos/queries.sql — константа с SQL, Params-структура, если параметров больше лимита, Row-структура для выборок с джойнами из двенадцатого урока и сами методы CreateTodo, ListTodos. Имя .sql.go наследуется от имени исходного файла — вот почему нарезку по файлам стоит делать осмысленной, а не «всё в один dump.sql». Наконец, querier.go — интерфейс со всеми методами пакета, появляется при emit_interface: true; одиннадцатый урок уже читал его применительно к транзакциям.

### Сгенерированный пакет глазами файловой системы

```go
internal/store/pgdb/
├── db.go            # DBTX, Queries, New, WithTx — каркас пакета
├── models.go        # Todo, User — структуры по таблицам схемы
├── querier.go       # интерфейс всех методов (emit_interface: true)
├── todos.sql.go     # методы из db/queries/todos/queries.sql
│                    #   константа createTodo, CreateTodoParams, метод CreateTodo
└── users.sql.go     # методы из db/queries/users/queries.sql
```

Карта соответствий, которую стоит запомнить: один файл запросов — один .sql.go, одна таблица — одна структура в models.go, весь пакет — один интерфейс в querier.go. Порядок файлов в пакетах queries ни на что не влияет: генератор сортирует сам, поэтому diff в git остаётся стабильным между запусками — генерация детерминирована, и на этом держится вся проверка в CI из двадцать второго урока.

### Проверка: где искать метод

**Вопрос:** Запрос -- name: ListUsers :many лежит в файле db/queries/users/queries.sql. В каком файле сгенерированного пакета окажется метод ListUsers?

- querier.go — интерфейс собирает все методы в одном файле
- models.go — методы хранятся рядом со структурами таблиц
- ✅ users.sql.go — имя генерата наследуется от имени файла запросов
- db.go — там живёт весь исполняемый код пакета

sqlc создаёт по одному .sql.go на каждый файл из пакетов queries, и имя наследуется от источника: users/queries.sql даёт users.sql.go. В querier.go метод только декларируется — это интерфейс, а не реализация; models.go хранит структуры таблиц и вообще не содержит методов; db.go — каркас DBTX/Queries/New/WithTx, без ваших запросов. Отсюда практический вывод для sqlc структура проекта: имя файла запросов — это имя файла кода, называйте файлы по домену.

### emit-ключи: рычаги формы генерата

Генератор настраивается не флагами командной строки, а ключами в gen.go записи — вторая серия их мелькала ещё во втором уроке, здесь самое время собрать карту целиком.emit_interface включает querier.go — интерфейс над всеми методами; для организации кода sqlc это главный шов: сервис будет зависеть от интерфейса, а не от структуры, и тесты из двадцать первого урока подменят реализацию без докера. emit_json_tags добавляет теги json к структурам моделей; удобно для маленьких сервисов, где модель таблицы сразу уезжает в ответ API, и спорно для больших, где слои стоит разводить. emit_pointers_for_null_types меняет представление nullable-колонок: вместо sql.NullString — *string; девятый урок разбирал эту развилку как вопрос вкуса и кодовой базы. emit_empty_slices заставляет методы :many возвращать пустой срез вместо nil — мелочь, которая убирает целый класс «if rows == nil» в обработчиках.

Дальше — ключи, управляющие именами и объёмом. emit_exact_table_names уже разобрали: имена структур без сингуляризации. omit_unused_structs режет из models.go структуры таблиц, на которые не ссылается ни один запрос — осмысленно в монорепозитории, где один конфиг собирает несколько пакетов, как в семнадцатом. query_parameter_limit задаёт порог: пока параметров не больше N (по умолчанию один), они идут в сигнатуру метода по отдельности, как только больше — sqlc упаковывает их в структуру <Query>Params. Ключ rename в gen.go переназначает имена колонок, конфликтующие с ключевыми словами или стилем: колонка order станет Order по вашему указанию, а не сломает генерацию.

### Карта gen.go: ключ → что меняет

| Ключ | Что делает | Когда включать |
| --- | --- | --- |
| emit_interface: true | генерирует querier.go — интерфейс всех методов | почти всегда: шов для тестов и подмен |
| emit_json_tags: true | теги json у полей структур | маленькие API без отдельных DTO |
| emit_pointers_for_null_types: true | nullable-колонки как *T вместо sql.Null*T | когда указатели понятнее обёрток (урок 09) |
| emit_empty_slices: true | :many возвращает [] вместо nil | чтобы не проверять срезы на nil |
| emit_exact_table_names: true | структура Authors вместо Author | когда сингуляризация врёт про домен |
| emit_methods_with_db_argument: true | методы принимают DBTX аргументом | общий Queries для базы и транзакций |
| omit_unused_structs: true | режет модели без ссылок из запросов | много записей sql в одном конфиге |
| query_parameter_limit: N | порог появления <Query>Params | хотите структуры даже для 1-2 параметров |
| output_db_file_name и соседи | переименовать db.go/models.go/querier.go | конфликты имён в общем каталоге |
| rename (gen.go) | карта колонка → имя поля Go | колонки-конфликты вроде order, user |

### Предскажите сигнатуру

```go
# sqlc.yaml (gen.go)
gen:
  go:
    package: "pgdb"
    out: "internal/store/pgdb"
    sql_package: "pgx/v5"
    query_parameter_limit: 1

-- db/queries/todos/queries.sql
-- name: RenameTodo :exec
UPDATE todos SET title = $1 WHERE id = $2 AND user_id = $3;
```

**Что выведет программа?**

- func (q *Queries) RenameTodo(ctx context.Context, title string, id, userID int64) error — параметры по одному
- ✅ func (q *Queries) RenameTodo(ctx context.Context, arg RenameTodoParams) error — параметров больше лимита, sqlc собрал структуру
- func (q *Queries) RenameTodo(ctx context.Context, arg RenameTodoArgs) error — суффикс Args
- func (q *Queries) RenameTodo(ctx context.Context, title string, id int64, userID int64) (Todo, error) — :exec возвращает строку

Правило порога: если параметров строго больше query_parameter_limit, sqlc генерирует структуру <Query>Params и метод принимает её одним аргументом. Три параметра против лимита один — значит, RenameTodoParams, и порядок полей в структуре повторяет порядок появления в запросе. Суффикса Args не существует — только Params. Аннотация :exec возвращает error и ничего больше — строки возвращает :one с RETURNING, что пятый урок показывал на вставках.

### Пакеты queries: нарезка, имена и что переиспользуется

Запросы — это единственный код, который вы пишете руками, поэтому вся дисциплина организации кода sqlc сходится к тому, как вы держите пакеты queries. Правило нарезки: папка — домен, файл — тема внутри домена. В todos лежат queries.sql с крудом и stats.sql с агрегациями из тринадцатого урока — и генерат раскладывается по файлам-близнецам, как вы уже знаете. Имена запросов делайте глагол+сущность: CreateTodo, ListUsers, CompleteTodo — они станут именами методов, и от этого зависит читаемость вызовов по всему проекту. Дубликат имени внутри одной записи sql — ошибка генерации: два CreateTodo в разных файлах одного пакета не соберутся, потому что в Go не может быть двух методов с одним именем на приёмнике.

Чего у sqlc нет — include-ов и макросов общего SQL: если пять запросов используют одно условие, придётся либо написать его пять раз, либо вынести его в базу как представление, либо собрать запрос на стороне Go из констант. Это осознанный минимализм: sqlc компилирует то, что видит, и не тащит препроцессор. Зато повторное использование параметров уже в языке запросов — [восьмой урок](/courses/sqlc/lessons/08-parametry/) показывал, как sqlc.arg(name) при повторном упоминании даёт один параметр, а не два. И не забывайте про sqlc.narg из девятого: nullable-параметры в UPDATE — половина «динамических» фильтров из пятнадцатого урока живёт именно на них, без всякой магии конкатенации.

### Два файла одного домена

```go
-- db/queries/todos/queries.sql: круд домена
-- name: CreateTodo :one
INSERT INTO todos (user_id, title, priority)
VALUES ($1, $2, $3)
RETURNING *;

-- name: ListTodosByUser :many
SELECT * FROM todos WHERE user_id = $1 ORDER BY created_at DESC;

-- db/queries/todos/stats.sql: аналитика того же домена
-- name: CountOpenTodos :one
SELECT count(*) FROM todos WHERE done = false;

-- name: OpenTodosByPriority :many
SELECT priority, count(*) AS cnt FROM todos
WHERE done = false GROUP BY priority ORDER BY priority;
```

Два файла — два .sql.go в генерате, и это удобно для навигации: код круда ищется в queries.sql.go, аналитика — в stats.sql.go. Имена — глагол плюс сущность: CreateTodo, ListTodosByUser, CountOpenTodos станут методами, которые читаются как предложения. Обратите внимание: оба файла живут в одной записи конфига, поэтому попадают в один пакет pgdb — отдельная запись sql, как в семнадцатом уроке, понадобилась бы только если бы домен требовал свой out или свои overrides.

### Переиспользование: репозиторий поверх Queries

Самый частый вопрос после анатомии — использовать ли сгенерированный пакет напрямую из обработчиков. Для маленького сервиса — да: pgdb.New(pool) в main, Queries в зависимостях хендлеров, и всё честно. Но в проекте, который растёт, есть две причины обернуть генерат в свой тонкий слой. Первая — транзакции: метод домена вроде «закрыть задачу и списать деньги» должен выполняться атомарно, и [одиннадцатый урок](/courses/sqlc/lessons/11-tranzakcii/) строил для этого хелпер withTx; если каждый обработчик сам открывает транзакцию, правила расползаются. Вторая — тесты: сервис, зависящий от интерфейса, проверяется фейком без базы, а интеграционные тесты из двадцать первого урока достанутся только обёртке.

Классический паттерн — структура-обёртка, которая встраивает *Queries и добавляет свою транзакционную дверь. Встраивание даёт красивую механику: все методы Queries становятся методами Store как есть, при этом Store может добавлять свои. Метод ExecTx у обёртки принимает функцию и передаёт ей *Queries, привязанную к транзакции через WithTx, — тот самый контракт одиннадцатого урока, но спрятанный за одной точкой входа. Границу не переступайте: обёртка без логики, без кэшей, без «умных» конвертеров — иначе вы построите второй ORM поверх генератора, который от ORM как раз уходил. Правило: Store — это про время жизни соединений и транзакций, а не про бизнес-правила.

### Store: встраивание Queries и одна дверь в транзакции

```go
// internal/store/store.go — наш код рядом с генератом
package store

import (
        "context"

        "github.com/jackc/pgx/v5/pgxpool"

        "tracker/internal/store/pgdb"
)

type Store struct {
        *pgdb.Queries // встраивание: методы Queries — теперь методы Store
        pool *pgxpool.Pool
}

func New(pool *pgxpool.Pool) *Store {
        return &Store{Queries: pgdb.New(pool), pool: pool}
}

// ExecTx — единственная дверь в транзакцию: контракт урока 11
func (s *Store) ExecTx(ctx context.Context, fn func(*pgdb.Queries) error) error {
        tx, err := s.pool.Begin(ctx)
        if err != nil {
                return err
        }
        defer tx.Rollback(ctx)

        if err := fn(s.Queries.WithTx(tx)); err != nil {
                return err
        }
        return tx.Commit(ctx)
}
```

Три строки, на которых держится паттерн. Встраивание *pgdb.Queries делает CreateTodo доступным как s.CreateTodo — обработчики не знают про pgdb.New. В ExecTx хелпер withTx из одиннадцатого урока превратился в метод: Begin, defer Rollback, вызов fn с Queries, привязанным к tx через WithTx, Commit на успехе. Вызов из домена: store.ExecTx(ctx, func(q *pgdb.Queries) error { ... }) — внутри функции доступны и запросы, и транзакционность, и ни один обработчик не видит Begin руками.

### Проверка: транзакция через обёртку

**Вопрос:** Store встраивает *pgdb.Queries. Как правильно выполнить CreateTodo и LedgerInsert в одной транзакции через Store?

- s.Queries.WithTx(tx) руками: открыть tx через s.pool.Begin и передать оба вызова
- ✅ s.ExecTx(ctx, func(q *pgdb.Queries) error { q.CreateTodo(...); return q.LedgerInsert(...) }) — метод-дверь делает Begin/Rollback/Commit сам
- Никак: sqlc не поддерживает транзакции внутри обёрток
- q.CreateTodo и q.LedgerInsert и так атомарны — каждая вставка это одна транзакция

Смысл обёртки — одна дверь: ExecTx открывает транзакцию, откладывает Rollback, передаёт функции Queries, привязанный к tx через WithTx, и коммитит на успехе; любая ошибка из функции откатывает всё. Вариант «руками» технически работает, но именно его паттерн и убирает — Begin по всему проекту возвращает шанс забыть Rollback. Два отдельных :exec атомарными не являются: между ними второй писатель увидит полузавершённое состояние — это и была мотивация одиннадцатого урока.

### Три движка в одном репозитории

Девятнадцатый урок закончился обещанием: конфиг, где postgresql, mysql и sqlite живут по соседству. Теперь у вас есть все детали, чтобы его выполнить. Каждая запись списка sql — свой пакет со своим out, это механика семнадцатого. Схема у движков общая по смыслу, но не по синтаксису: SERIAL против INTEGER PRIMARY KEY, TIMESTAMPTZ против TEXT — поэтому схема разделяется на db/schema/pg, db/schema/my, db/schema/lite, и тут же встаёт вопрос о пакетах queries: дублировать ли SQL трижды?

Честный ответ зависит от того, насколько диалекты расходятся. Если запросы — фильтры, вставки, обновления на голом SQL без экзотики, один каталог db/queries обслуживает все три записи: массив queries может указывать на одну и ту же папку из разных записей, а плейсхолдеры ? и $1 останутся единственным расходящимся штрихом, и sqlc молча переварит оба стиля в своём парсере. Как только появляются массивы ANY($1::int[]), ON CONFLICT, RETURNING с особенностями — выносите специфичное в db/queries/pg и указывайте его только в PG-записи: общий каталог минус папка специфики. Помните и о картах типов из [восемнадцатого](/courses/sqlc/lessons/18-mysql/) и [девятнадцатого уроков](/courses/sqlc/lessons/19-sqlite/): DECIMAL в MySQL — string, в SQLite — float64, то есть одна и та же колонка даёт разные поля в соседних пакетах, и обёртка Store — правильное место, чтобы разницу проглотить. Трекер из курса живёт на PostgreSQL, а SQLite-пакет обслуживает тесты и CLI — это ровно та модель, где мультидвижковость не превращается в налог.

### Конфиг мультидвижкового репозитория

```go
# sqlc.yaml — три записи, три пакета
version: "2"
sql:
  - engine: "postgresql"
    schema: "db/schema/pg"
    queries: ["db/queries/common", "db/queries/pg"]
    gen:
      go:
        package: "pgdb"
        out: "internal/store/pgdb"
        sql_package: "pgx/v5"

  - engine: "mysql"
    schema: "db/schema/my"
    queries: ["db/queries/common"]
    gen:
      go:
        package: "mydb"
        out: "internal/store/mydb"

  - engine: "sqlite"
    schema: "db/schema/lite"
    queries: ["db/queries/common"]
    gen:
      go:
        package: "sqlitedb"
        out: "internal/store/sqlitedb"
```

Один запуск sqlc generate собирает три пакета: pgdb — полный, с доменной спецификой из db/queries/pg; mydb и sqlitedb — общий набор из db/queries/common. Схемы разнесены по папкам движков, потому что синтаксис DDL расходится раньше, чем запросы. Каждая запись компилируется независимо: ошибка в mysql-запросе не помешает сборке pgdb. Такие же три пакета вы собирали по одному в восемнадцатом и девятнадцатом — здесь они просто живут под одной крышей.

> **Не редактируйте *.sql.go руками.** Соблазн вечный: подправить сгенерированный метод, добавить поле в структуру, закрыть тип «кривой» аннотацией. Первый же sqlc generate затрёт правки молча и без вопросов — и вы узнаете об этом по падению компиляции или, хуже, по тихо изменившемуся поведению. Всё, что хочется изменить, настраивается выше по течению: типы — через overrides из четвёртого урока, имена колонок — через rename, форма пакета — через emit-ключи этой карты, SQL — в файлах queries. Правило организации кода sqlc простое: генерат — это артефакт сборки, источник — sqlc.yaml плюс SQL.

### Впишите ключи организации пакета

Заполните пропуски: ключ интерфейса методов, ключ тегов json, порог Params-структур и каталог, в который попадает генерат.

```go
# sqlc.yaml
version: "2"
sql:
  - engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries"
    gen:
      go:
        package: "pgdb"
        out: "internal/store/pgdb"
        ___: true          # querier.go: интерфейс всех методов
        ___: true          # теги json у структур
        ___: 1             # больше одного параметра — структура <Query>Params
```

Подсказка: Первые два ключа начинаются с emit_, третий — число, и он же правил сигнатуру RenameTodo из predict выше.

Правильные ответы в пропусках: `emit_interface`, `emit_json_tags`, `query_parameter_limit`

### Коммитить генерат или генерировать на месте

Последнее организационное решение — судьба сгенерированных файлов в git, и оно делит сообщество надвое. Аргументы за коммит: пакет собирается у любого, кто склонировал репозиторий, без установки sqlc; diff генерата в pull request показывает ровно то, что изменит запрос — ревьюер видит новые поля структур и новые методы; версии sqlc у всех команды и в деплое совпадают по умолчанию. Аргументы против: PR шумит сгенерированными строками, а если кто-то забудет перегенерировать, репозиторий уезжает в дрейф — источники говорят одно, генерат другое.

Дрейф лечится механикой, а не запретами:sqlc diff из двадцать второго урока сравнит генерат с тем, что выдал бы текущий конфиг, и уронит CI при расхождении. Практика, которая работает в большинстве команд: коммитим генерат, CI проверяет diff, версия sqlc зафиксирована. Фиксация делается прямо в go:generate-директиве: go run с явной версией модуля тянет именно её, и генерация перестаёт зависеть от того, что установлено у разработчика в PATH. Экономия на коммите генерата (репозиторий тоньше, diff чище) почти всегда оборачивается tax-ом на сборку: каждому потребителю нужен установленный sqlc, а откат релиза — это не revert-коммит, а повторная генерация на месте. Оставьте «генерим на месте» проектам, где слой данных меняется каждый день, и вернитесь к нему, когда CI из двадцать второго урока научится проверять всё сам.

### go:generate: версия генератора в коде

```go
// tools/generate.go — директива с зафиксированной версией
package tools

//go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0 generate

// Запуск: go generate ./tools
// Второй путь — бинарник в PATH:
//   sqlc generate   (версию держим одинаковой через CI и README)
```

Директива go:generate превращает генерацию в команду go generate ./... — одну для всего репозитория, вместе с ent-проектами или protobuf-задачами, если появятся. Явная версия в пути модуля (@v1.27.0) — это пиннинг: у всех разработчиков и в CI сгенерируется одинаковый код, и diff-проверка не будет падать из-за разницы версий инструмента. Альтернатива — бинарник sqlc в PATH: быстрее, но версию придётся синхронизировать вне git.

### Соберите переезд на каноничную раскладку

Расставьте шаги перевода проекта на sqlc структуру проекта из урока.

Правильный порядок строк:

1. `Разложить схему по db/migrations: пары up/down из шестнадцатого урока`
2. `Разбить запросы на домены: db/queries/todos, db/queries/users`
3. `Переписать sqlc.yaml: queries списком каталогов, out в internal/store/pgdb`
4. `Добавить emit_interface: true — шов для будущих тестов`
5. `Запустить sqlc generate и закоммитить обновлённый генерат`
6. `Завернуть Queries в Store с ExecTx — дверь в транзакции из урока 11`
7. `Перевести обработчики на Store: pgdb.New больше не зовётся в main`
8. `Включить sqlc diff в CI — дрейф генерата теперь ловит конвейер`

### Пять правил организации кода sqlc

- Схема в db/migrations, запросы в db/queries/<домен>, генерат в internal/store/<pkg> — три места, которые ищутся без grep.
- Один файл запросов — один .sql.go: имя файла — часть публичного устройства пакета.
- Имена запросов — глагол плюс сущность: они становятся методами, которые вы читаете каждый день.
- Генерат не редактируется: все правки через sqlc.yaml, overrides, rename и SQL-файлы.
- Обёртка Store — про соединения и транзакции, а не про бизнес-логику; иначе это второй ORM.

### Что дальше: структура, которая готова к тестам

Оглядитесь: раскладка стоит, пакет собран из четырёх сортов файлов, emit-карта настроила форму, Store закрыл транзакции, а мультидвижковый конфиг держит три мира в одном репозитории. Если запрос «sqlc организация кода» сворачивать в один ответ, ответом будет эта раскладка плюс договорённости вокруг генерата — всё остальное в курсе оказалось её следствиями. Главная причина, по которой всё это делалось именно так, а не «лишь бы компилировалось» — тесты. Интерфейс querier.go — это место, куда сервис может встать на учет и не звать базу в юнит-тестах; DBTX — шов, через который генерат равнодушен к тому, живая база перед ним, sqlite в памяти или мок; Store с ExecTx — единственная точка, которую нужно проверять на атомарность. Двадцать первый урок разложит это хозяйство по пирамиде: юнит-тесты на фейках репозитория, интеграционные на sqlite из девятнадцатого и на настоящем Postgres, sqlmock — и покажет, где моки врут, а где честнее самой базы. Если вы ещё не включили emit_interface — сделайте это сегодня: завтра будет поздно, потому что сервисы уже встанут на конкретную структуру, а не на интерфейс.

### Карточки: организация кода

- **Каноничная раскладка sqlc-репозитория?** — db/migrations — схема (тот же DDL, что накатывают миграции), db/queries/<домен> — пакеты queries, internal/store/<pkg> — генерат под защитой internal, sqlc.yaml в корне
- **Четыре сорта файлов сгенерированного пакета?** — db.go (DBTX, Queries, New, WithTx), models.go (структуры таблиц), <файл>.sql.go (методы по файлам запросов), querier.go (интерфейс при emit_interface)
- **Когда появляется структура <Query>Params?** — Когда параметров больше query_parameter_limit (по умолчанию 1); поля в порядке появления в запросе; имя — <Query>Params, не Args
- **Зачем emit_interface и emit_methods_with_db_argument?** — emit_interface — querier.go, интерфейс для подмен в тестах; emit_methods_with_db_argument — методы принимают DBTX аргументом, один Queries на базу и транзакции
- **Как устроен Store поверх Queries?** — Встраивание *pgdb.Queries (методы доступны напрямую) + метод ExecTx: Begin, defer Rollback, fn(Queries.WithTx(tx)), Commit — одна дверь в транзакции урока 11
- **Коммитить генерат или генерировать на месте?** — По умолчанию коммит: diff в ревью, сборка без sqlc, детерминированность; дрейф ловит sqlc diff в CI; версию пиним через go run ...cmd/sqlc@v1.27.0 в go:generate

## Коротко: что запомнить

- sqlc структура проекта: db/migrations для схемы, db/queries/<домен> для пакетов queries, internal/store/<pkg> для генерата, sqlc.yaml в корне; queries и schema принимают списки путей.
- Сгенерированный пакет — четыре сорта файлов: db.go (DBTX, Queries, New, WithTx), models.go (структуры таблиц), <файл>.sql.go (методы по одному на файл запросов), querier.go (интерфейс при emit_interface).
- Форму генерата правят ключи gen.go: emit_interface, emit_json_tags, emit_pointers_for_null_types, emit_empty_slices, emit_exact_table_names, omit_unused_structs, query_parameter_limit, rename.
- Переиспользование: обёртка Store встраивает *Queries и даёт ExecTx (Begin, defer Rollback, fn с WithTx, Commit) — транзакционная дверь одиннадцатого урока в одном месте.
- Три движка в одном репозитории: запись списка sql на пакет, схемы по папкам движков, общие запросы в db/queries/common, специфика — в db/queries/pg; карты типов движков различают одни и те же колонки.
- Генерат коммитим и не правим руками; версию sqlc пиним в go:generate (go run ...@v1.27.0); дрейф ловит sqlc diff — тема двадцать второго урока.

## Домашнее задание

Первое: переведите трекер на раскладку урока — db/queries/todos и db/queries/users с двумя файлами в первом домене, out в internal/store/pgdb, queries списком каталогов; убедитесь, что в пакете появились todos.sql.go и users.sql.go, и что имена файлов совпали с вашими. Второе: включите emit_interface и напишите в cmd маленькую функцию, принимающую pgdb.Querier, — вызовите её с *pgdb.Queries и убедитесь, что компилятор доволен. Третье: поставьте query_parameter_limit: 1 и найдите в генерате запрос, у которого Params-структура появилась после смены ключа, — сравните сигнатуры до и после. Четвёртое: соберите Store с ExecTx, закройте две вставки в одной транзакции и проверьте откат: верните ошибку из fn и убедитесь, что ни одна строка не записалась. Пятое: добавьте sqlite-запись в тот же конфиг по образцу девятнадцатого урока, укажите общий каталог queries и одну PG-специфичную папку — посмотрите, какие запросы действительно собираются на обоих движках, а какие требуют выноса. Шестое: попробуйте отредактировать *.sql.go руками, запустите sqlc generate и опишите письменно, что произошло, — эта шпаргалка пригодится, когда соблазн вернётся. Седьмое: добавьте go:generate-директиву с пином версии и проверьте, что go generate ./... даёт diff, идентичный вашему бинарнику sqlc той же версии.

## Вопросы и ответы по уроку

### Можно ли держать один каталог queries для трёх движков сразу?

Да, пока запросы не используют специфичный диалект: массив queries в разных записях sql может указывать на одну и ту же папку, и каждый движок скомпилирует её своей картой типов. Как только появляются PG-массивы, ON CONFLICT или SQLite-праги в запросах, специфичное выносится в отдельную папку (db/queries/pg) и подключается только в соответствующей записи. Схема при этом почти всегда разделяется: DDL у движков расходится раньше запросов.

### Где должен лежать sqlc.yaml — в корне или рядом с сервисом?

В корне, пока сервис один: команда sqlc generate без аргументов находит его в текущей папке, и вся генерация репозитория — одна команда. Несколько конфигов оправданы в монорепозитории, где сервисы независимы и генерация одного не должна триггерить другой; в этом случае конфиг кладут рядом с сервисом и зовут sqlc generate -f path/to/sqlc.yaml. Промежуточный вариант — один конфиг с несколькими записями sql, как в семнадцатом и этом уроке.

### Почему не стоит оборачивать Queries в толстый репозиторий с логикой?

Потому что вы получите ORM, от которого sqlc вас освободил, только написанный своими руками и без документации. Сгенерированный код уже даёт типобезопасные методы с честными структурами; обёртка оправдана ровно в двух ролях — управление соединениями и транзакциями (ExecTx) и изоляция интерфейса для тестов. Конвертеры бизнес-объектов, кэши и правила валидации живут выше, в доменном слое, где они проверяются юнит-тестами без базы.
