---
title: "Несколько схем и организация"
description: "sqlc схемы и организация проекта: CREATE SCHEMA и search_path глазами генератора, sqlc.yaml multiple packages, разделение доменов и общие модели."
course: "sqlc: типобезопасный код из SQL-запросов"
url: "https://golangmaster.ru/courses/sqlc/lessons/17-schemy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Несколько схем и организация

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

Тема урока — sqlc схемы и организация проекта: момент, в который трекеру становится тесно в одном пакете queries. Шестнадцать уроков канон держался на минимализме: одна база tracker, одна схема public, один пакет db с одним Querier, в котором рядом живут задачи, пользователи и журнал списаний. Пока методов было два десятка, это было достоинством; но сгенерированный интерфейс вырос до экрана прокрутки, папка db/queries превратилась в плоский список файлов разной доменной принадлежности, а бухгалтерия спрашивает, почему её таблица лежит в одной куче с задачами. Урок отвечает на четыре вопроса. Первый: что такое схемы Postgres и зачем домену собственное пространство имён — CREATE SCHEMA billing и квалифицированные имена. Второй: как sqlc видит это устройство — search_path генератор не эмулирует, неквалифицированные имена ищет только в public, а таблицам вне умолчания выдаёт Go-имена с префиксом схемы. Третий: sqlc.yaml multiple — список sql, в котором каждая запись собирает собственный Go-пакет: schema, queries и gen.go на пакет. Четвёртый: как жить с моделями на стыке — дубль усекается omit_unused_structs, имена файлов настраиваются, общие типы выносятся глобальными overrides. Организация sqlc — это не про красоту каталогов, а про границы, которые компилятор умеет охранять. Конфиг, который мы разветвляем, завёлся во [втором уроке](/courses/sqlc/lessons/02-nastrojka/); папка миграций как источник схемы — в [шестнадцатом](/courses/sqlc/lessons/16-migracii/); план остальных тем — на странице [курса sqlc](/courses/sqlc/).

К семнадцатому уроку у трекера три таблицы и десятки методов. Задачи с их фильтрами и страницами — сердце продукта; пользователи — исполнители, с которыми задачи соединяются джойном; журнал списаний — бухгалтерский домен, куда одиннадцатый урок положил суммы за закрытые задачи. Всё это десятилетиями (то есть шестнадцатью уроками) жило в одном пакете db: одна папка db/queries с файлами по темам, один models.go, один Querier, в котором CreateSpending стоит соседом с SearchTodos. Пока доменов было ноль, а тем — много, это работало.

Потом появляются боли, каждая из которых знакома любому, кто обслуживал выросший проект. Первая: Querier — это список всех методов проекта сразу, и на ревью вопроса «кто владеет этим запросом» отвечает только комментарий. Вторая: файлы в db/queries перемешаны по доменам — spendings.sql лежит рядом с pagination.sql, и ничто, кроме конвенции, не мешает новому коллеге добавить бухгалтерский запрос в файл задач. Третья: изменения домена не видны на границе — из сигнатур не видно, что панель фильтров не имеет права трогать журнал списаний.

У Postgres для этого есть встроенный инструмент — схемы: пространства имён внутри одной базы. У sqlc — своя часть ответа: несколько записей в sqlc.yaml, каждая со своей папкой queries и собственным Go-пакетом на выходе. Урок соединяет оба уровня: сначала схема в базе и то, как генератор её читает, потом пакеты в конфиге и то, как они делят код по доменам.

> **Как проверять себя в этом уроке.** Честно о формате, как и во всём курсе: тема требует живой базы и локального генератора, поэтому уроки не выполняются в песочнице — runner-ов здесь нет. Понимание проверяют quiz, predict, fill, order и флешкарты: они гоняют те же факты, что вы повторите командами CREATE SCHEMA и sqlc generate у себя. Поведение генератора сверено с документацией sqlc (раздел Configuration: список sql, ключи gen.go, omit_unused_structs, output_models_file_name, глобальные overrides) и с исходниками генератора: разбор CREATE SCHEMA, префикс схемы в Go-именах, разрешение неквалифицированных имён только через схему по умолчанию.

### sqlc схемы: домены внутри одной базы

Схема в Postgres — это пространство имён внутри одной базы: в ней живут таблицы, перечисления, представления и функции. Иерархия выглядит так: кластер содержит базы, база содержит схемы, схема содержит таблицы. В каждой базе есть схема по умолчанию — public, и шестнадцать уроков трекер честно прожил в ней: CREATE TABLE без квалификатора создаёт таблицу именно там. Схема — не база: это самая частая путаница темы. Разделение доменов по схемам не разводит данные по разным серверам и даже по разным файлам на диске — оно разводит имена и правила внутри одной базы.

Зачем домену собственное пространство имён. Первое — границы видны в самом SQL: запрос к billing.spendings сам объявляет, что трогает бухгалтерию, и в ревью это видно без знания контекста. Второе — права: Postgres выдаёт доступы на уровне схемы, и можно завести роль, которой разрешено читать billing и запрещено менять public — segregated доступ без единой строчки кода приложения. Третье — имена: у схемы свой словарь, и таблица журнала может называться просто spendings, не споря с будущими сущностями задач. Четвёртое — жизненный цикл: домен можно выгрузить одной командой DROP SCHEMA, не выписывая по таблице.

Применяем к трекеру: журнал списаний из одиннадцатого урока переезжает в схему billing — выделенное пространство бухгалтерии. Важная деталь, о которой часто спотыкаются: внешний ключ через схемы законен. billing.spendings может ссылаться на todos из public — обе таблицы живут в одной базе, и ссылочная целостность не знает о наших доменных границах. Домены — договор людей; база продолжает видеть одно пространство данных.

### Схема billing: шаг, которым домен получает имя

```go
-- db/migrations/000004_billing.sql: домен списаний получает собственную схему
CREATE SCHEMA billing;

CREATE TABLE billing.spendings (
    id BIGSERIAL PRIMARY KEY,                       -- int64 в Go
    todo_id BIGINT NOT NULL REFERENCES todos (id),  -- внешний ключ через схемы законен
    amount BIGINT NOT NULL,                         -- сумма в копейках
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()   -- время ставит база
);

-- db/queries/spendings/spendings.sql: имена квалифицированы схемой
-- name: CreateSpending :exec
INSERT INTO billing.spendings (todo_id, amount)
VALUES ($1, $2);
```

Два движения в одном шаге. Первое: CREATE SCHEMA billing — таблица создаётся сразу с префиксом, потому что «текущей схемы» у sqlc не существует: генератор не исполняет SET, и после CREATE SCHEMA ничего не переключается — в живой базе тоже лучше не полагаться на переключение, а писать полные имена. Второе: запрос уже квалифицирован — INSERT INTO billing.spendings; это и есть тот стиль, который переживёт разветвление конфига ниже. Миграция 000004 продолжает нумерацию канона: базовая схема, приоритет, мягкое удаление — и теперь домен бухгалтерии.

### search_path: как sqlc видит неквалифицированные имена

В живой базе за короткие имена отвечает search_path — список схем, в которых Postgres ищет неквалифицированные ссылки: SET search_path TO billing, public, и запрос FROM spendings найдёт таблицу в billing, если там она есть. Удобно, но именно на этом месте генератор и база расходятся в поведении, и урок обязан сказать это прямо: search_path sqlc не эмулирует. Каталог генератора собирается из schema-файлов, и неквалифицированное имя в запросе разрешается только в схеме по умолчанию — public. Настройки search_path в конфиге не существует: вопрос о ней в репозитории sqlc открыт как enhancement (Configure search_path), а оператор SET генератор не исполняет вовсе.

Практическое следствие одно, но железное: в проекте с несколькими схемами всё, что лежит вне public, пишется квалифицированно — и в DDL, и в запросах. Короткое FROM spendings при таблице billing.spendings не «не найдётся тихо» — генерация честно упадёт с ошибкой вида relation "spendings" (код 42P01, неопределённая таблица); падение здесь — лучший исход, потому что оно ловится на ноутбуке.

Есть у схем и подарочная сторона — имена в Go. Для таблиц вне схемы по умолчанию sqlc строит имя по шаблону схема_таблица: billing.spendings превращается в структуру BillingSpending, а перечисление billing.payment_status — в тип BillingPaymentStatus с константами вида BillingPaymentStatusCaptured. В public всё остаётся как прежде — канонический Todo без префиксов. Границы домена таким образом читаются прямо в коде приложения: один взгляд на сигнатуру, и видно, чью таблицу вы держите в руках.

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

```go
-- db/migrations/000004_billing.sql: схема и домен
CREATE SCHEMA billing;

CREATE TYPE billing.payment_status AS ENUM ('captured', 'refunded');

CREATE TABLE billing.spendings (
    id BIGSERIAL PRIMARY KEY,
    todo_id BIGINT NOT NULL REFERENCES todos (id),
    amount BIGINT NOT NULL,
    status billing.payment_status NOT NULL DEFAULT 'captured'
);

-- db/queries/spendings/spendings.sql
-- name: CreateSpending :exec
INSERT INTO billing.spendings (todo_id, amount, status)
VALUES ($1, $2, $3);
```

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

- Spending и PaymentStatus: схема не влияет на Go-имена
- ✅ BillingSpending и BillingPaymentStatus: к имени добавляется префикс схемы
- Billing_spendings и Billing_payment_status: подчёркивания префикса сохраняются в Go
- Spendings и PaymentStatus: sqlc не поддерживает CREATE SCHEMA и падает до генерации

Для схем вне умолчания генератор строит имя по формуле схема_таблица: billing_spendings, а дальше вступает обычный конвейер — подчёркивания исчезают, множественное число единится, и на выходе BillingSpending. Перечисление единения не проходит, зато проходит тот же префикс: billing_payment_status → BillingPaymentStatus, а его значения становятся константами BillingPaymentStatusCaptured и BillingPaymentStatusRefunded. Третий вариант помнит про формулу, но забыл про подчёркивания; первый — про префикс; четвёртый неверен в корне: CREATE SCHEMA генератор разбирает штатно, схема попадает в его каталог. В схеме по умолчанию public тот же код дал бы Spending и PaymentStatus без префиксов — именно поэтому канонический Todo и выглядел так просто.

### Проверка: короткое имя против квалифицированного

**Вопрос:** Таблица создана как billing.spendings, в каталоге sqlc схема billing существует. Коллега пишет запрос SELECT * FROM spendings без квалификатора и запускает sqlc generate. Что произойдёт?

- Сгенерируется: генератор найдёт таблицу перебором всех схем, как это делает search_path в базе
- Сгенерируется с предупреждением: sqlc подставит схему billing автоматически, потому что она одна содержит такую таблицу
- ✅ Генерация упадёт: неквалифицированное имя ищется только в public, и там таблицы spendings нет
- Сгенерируется пустая структура: колонки неизвестны, sqlc продолжит работу, что сможет

Правило разрешения имён жёсткое: без квалификатора генератор смотрит только в схему по умолчанию — public. Search-path-логики «перебери схемы по порядку» у него нет, и настройки, которая её включила бы, не существует. В public таблицы spendings нет — генерация падает с сообщением о несуществующей таблице, и это хорошо: ошибка поймана до компиляции. Спасение — написать FROM billing.spendings. Автоподстановки не будет даже если таблица с таким именем существует ровно в одной схеме: явность — цена и валюта предсказуемой генерации.

### sqlc.yaml multiple: список sql, пакет на запись

Теперь сторона конфига. Ключ sql в sqlc.yaml — не объект, а список: каждая запись — самостоятельный набор генерации со своим движком, своей схемой, своими запросами и своим генератором кода. Доки прямо показывают конфиг из двух записей с разными движками; у каждой записи набор ключей один: name — необязательный человекочитаемый идентификатор набора, engine, schema — папка миграций или файл, queries — папка запросов или список путей, gen — генератор, плюс database и rules для проверок. Ключ name при одном пакете бесполезен, при двух-трёх уже помогает читать конфиг: записи перестают быть безымянными блоками yaml.

Главное следствие списка: на каждую запись — свой Go-пакет. В gen.go пишутся package — имя пакета на языке Go, и out — каталог, куда ляжет сгенерированный код; разные out — разные каталоги — разные пакеты с независимыми импортами. Кто-то ищет это под словами sqlc.yaml multiple packages, и поисковик приводит ровно сюда: список sql — штатный, документированный способ держать в одном файле конфигурации сколько угодно наборов генерации.

И второе следствие, менее очевидное: записей можно сколько угодно, а команда одна. sqlc generate обходит весь список за один запуск — каждый пакет обновляется сам. Так прячется ещё и ответ на вопрос об организации sqlc: один файл конфигурации, несколько точек выхода, границы домена видны и в конфиге, и в импортах. Соседняя запись может отличаться даже движком: в доках это postgresql рядом с mysql; для трекера обе записи — postgresql, но дорога к восемнадцатому уроку про MySQL уже лежит в этой конструкции — конфиг к смене движка готов, останется выучить отличия синтаксиса.

### Канон урока: один конфиг, два пакета

```go
# sqlc.yaml: список sql — по записи на домен
version: "2"
sql:
  - name: "todos"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/todos"
    gen:
      go:
        package: "tododb"
        out: "internal/store/tododb"
        sql_package: "pgx/v5"

  - name: "spendings"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/spendings"
    gen:
      go:
        package: "spendingdb"
        out: "internal/store/spendingdb"
        sql_package: "pgx/v5"
```

Разберём канон построчно. sql — список из двух записей, у каждой своё имя: todos и spendings. schema у обеих записей один — db/migrations: папка миграций остаётся единственным источником правды о схеме, как договорились в шестнадцатом уроке; обе записи собирают один и тот же каталог базы и потому видят одни и те же таблицы — включая чужой домен. Различаются записи queries (собственная папка домена) и gen.go: tododb едет в internal/store/tododb, spendingdb — в internal/store/spendingdb. Один sqlc generate обработает обе записи и обновит оба каталога. sql_package: pgx/v5 наследуется из канона второго урока — у обоих пакетов один драйвер, поэтому транзакционные сценарии через два домена остаются совместимыми.

### Проверка: сколько запусков генерации

**Вопрос:** В sqlc.yaml две записи sql: tododb и spendingdb. Разработчик поправил запрос в db/queries/spendings и хочет обновить сгенерированный код. Сколько раз нужно запустить sqlc generate?

- Два раза — по одному на запись, и порядок запусков важен
- ✅ Один раз: команда обходит все записи списка и обновляет каждый out
- Ни разу: правка подхватится только при следующей сборке проекта
- Один раз, но только если записи используют один и тот же движок

Список sql обрабатывается целиком: один запуск sqlc generate проходит по всем записям конфига и перегенерирует каждый указанный каталог. Порядок не важен — записи независимы; сборка проекта тут ни при чём — генерация ручная и явная; одинаковый движок на запуски не влияет — разнородные записи едут тем же циклом. Именно поэтому список записей стоит завести, как только пакетов стало два: конфиг остаётся одной точкой входа, и команда «перегенерируй всё» не превращается в скрипт с перечислением пакетов.

### Разделение доменов: свои queries и свой пакет

Конвенция курса «одна тема выборок — один файл» выросла в правило уровнем выше: один домен — папка. В db/queries/todos живут todos.sql — списки, фильтры и джойны задач — и users.sql — домен исполнителей, который задачами пользуется, но владеет своими таблицами; сюда же переезжают панель фильтров и страницы. В db/queries/spendings — spendings.sql, журнал списаний со своей схемой billing. Ключ queries у записи принимает папку — или список путей, если домену нужно собрать файлы из нескольких мест; ни один файл не принадлежит двум пакетам, и это не ограничение, а граница: запрос можно найти по домену, домен — по пакету.

Каждый пакет получает полный набор сгенерированных файлов: свой db.go с конструктором и интерфейсом соединения, свои модели, свой Querier — интерфейс, в котором только методы этого домена. Граница становится компиляторной: сервис задач принимает *tododb.Queries и физически не может вызвать CreateSpending — такого метода в его типе нет.

Где жить запросам на стыке? Правило владельца: запрос принадлежит домену главного данных. ListTodosWithUser — джойн задач с исполнителями — живёт в todos.sql: задачу показывает пользовательский интерфейс, пользователь здесь деталь задачи. Транзакция из одиннадцатого урока — закрыть задачу и записать списание — затрагивает оба домена, и с двумя пакетами сценарий не ломается: оба пакета построены над одним контрактом соединения, на одном транзакционном соединении можно держать по объекту Queries от каждого домена; подробности механики — в том уроке, здесь важна фактура: разветвление пакетов не ломает транзакции, потому что делит методы, а не соединения.

### Дерево проекта и два импорта вместо одного

```go
# дерево после разветвления: домены получили папки и пакеты
db/
  migrations/            # одна папка схемы на все пакеты
    000001_init.up.sql
    000002_add_priority.up.sql
    000003_soft_delete.up.sql
    000004_billing.sql
  queries/
    todos/               # запись "todos" -> пакет tododb
      todos.sql          # задачи: списки, фильтры, джойны
      users.sql          # исполнители: свой домен в том же пакете
    spendings/           # запись "spendings" -> пакет spendingdb
      spendings.sql      # журнал: схема billing
internal/
  store/
    tododb/              # сгенерировано: db.go, models.go, querier.go
    spendingdb/          # сгенерировано: db.go, billing_models.go, querier.go

// internal/service/dashboard.go: два домена — два импорта
import (
        "tracker/internal/store/spendingdb"
        "tracker/internal/store/tododb"
)

// Dashboard держит по объекту Queries на домен: задачи в tododb,
// суммы — в spendingdb, где таблицы живут в схеме billing
type Dashboard struct {
        Todos     *tododb.Queries
        Spendings *spendingdb.Queries
}
```

Как читать дерево. Миграции не разветвились: сумма up-файлов по-прежнему описывает всю базу, и оба пакета смотрят на неё как на источник схемы. Запросы разветвились строго по доменам. Путь импорта складывается из модуля и out — tracker/internal/store/tododb, и в коде приложения теперь два импорта вместо одного. Имя billing_models.go в spendingdb — не опечатка: имя файла моделей настраивается, и на этом месте урока мы его настроим; по умолчанию файл называется models.go. Каждая из структур Queries компилируется независимо — правка домена задач не трогает журнал списаний.

### Модели на стыке: кто получит какие структуры

Раз оба пакета смотрят на одну папку миграций, честный вопрос — что окажется в моделях каждого. Ответ генератора прост до грубости: models.go каждого пакета строится по всему входу схемы. Раз в db/migrations описаны todos, users и billing.spendings, то и Todo, и User, и BillingSpending появятся в обоих пакетах — независимо от того, чьи запросы лежат в папке. Это не баг, а следствие устройства: генератор печатает модели по каталогу, а не по упоминаниям в запросах.

Дубль — не всегда зло: пакеты независимы, их можно версировать и переписывать порознь, и для среднего проекта две копии структур — приемлемая плата за простоту. Но у конструкции есть три ручки настройки, и урок перечисляет все три. Первая — omit_unused_structs: включённая, она оставляет в пакете только те структуры таблиц и перечислений, которые реально встречаются в его запросах; spendingdb перестаёт таскать Todo, если его запросы не касаются задач. Вторая — имена файлов: output_models_file_name меняет models.go на что угодно — например, billing_models.go; рядом живут output_db_file_name и output_querier_file_name для db.go и querier.go. Третья — само входное множество: schema может быть списком путей, и можно было бы развести миграции по доменам; но тогда внешний ключ billing.spendings → todos перестанет проверяться генератором в пакете spendings, потому что todos уйдёт из его поля зрения. Общая папка — компромисс, при котором границы проверяются целиком; усечение моделей дешевле усечения схемы.

### Предскажите содержимое models.go

```go
# sqlc.yaml — обе записи смотрят на одну схему:
#   schema: "db/migrations"   (там: todos, users, billing.spendings)
#   omit_unused_structs не включён — настройки по умолчанию

-- db/queries/spendings/spendings.sql — единственный файл записи "spendings"
-- name: CreateSpending :exec
INSERT INTO billing.spendings (todo_id, amount) VALUES ($1, $2);

-- name: TotalSpent :one
SELECT sum(amount)::bigint FROM billing.spendings WHERE todo_id = $1;
```

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

- Только BillingSpending: пакет не видит чужих таблиц
- ✅ Todo, User и BillingSpending: модели строятся по всему входу схемы, независимо от запросов пакета
- Ничего: models.go создаётся только для первой записи конфига
- Ошибка генерации: одна папка схемы не может обслуживать две записи

По умолчанию генератор печатает модели по всей схеме из schema-входа: в internal/store/spendingdb/models.go попадут Todo, User и BillingSpending, хотя запросы пакета касаются только журнала. Никакой связи «модель существует ⟺ она упомянута в запросах» без настройки нет; первая запись конфига не имеет приоритета, а одна схема честно обслуживает сколько угодно записей. Лекарство известно из текста выше: omit_unused_structs: true — и в spendingdb останутся только структуры, которые трогают его запросы. Имена row-структур вроде TotalSpentRow — отдельная история: они рождаются из запросов и не зависят от этого переключателя.

### Усечение моделей и имя файла

```go
# sqlc.yaml: обе записи целиком — ручки настройки на месте
version: "2"
sql:
  - name: "todos"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/todos"
    gen:
      go:
        package: "tododb"
        out: "internal/store/tododb"
        sql_package: "pgx/v5"
        omit_unused_structs: false   # фасад задач: пусть несёт все модели

  - name: "spendings"
    engine: "postgresql"
    schema: "db/migrations"
    queries: "db/queries/spendings"
    gen:
      go:
        package: "spendingdb"
        out: "internal/store/spendingdb"
        sql_package: "pgx/v5"
        omit_unused_structs: true        # только то, что трогают запросы пакета
        output_models_file_name: "billing_models.go"
```

Канон с ручками. omit_unused_structs выключен в tododb — пакет задач остаётся общим фасадом, где модели всех таблиц под рукой; включён в spendingdb — бухгалтерский пакет держит только собственное. Усечение касается именно табличных моделей и перечислений: структуры параметров и результатов вроде CreateSpendingParams и TotalSpentRow рождаются из запросов и живут всегда. output_models_file_name задаёт имя файла моделей вместо models.go по умолчанию — в дереве выше spendingdb уже показан с billing_models.go; соседние ключи тем же способом переименовывают db.go и querier.go, если в одном каталоге генерируется несколько наборов. Один запуск sqlc generate обновит оба пакета с новыми настройками — ручки не требуют отдельной команды.

### Общие типы: глобальные overrides

Остаётся стык, который не решается ни схемами, ни пакетами: один и тот же смысл в двух доменах. Суммы в копейках хранятся BIGINT и в задачах (стоимость), и в журнале (списания); в Go хочется видеть их не голым int64, а типом Money из общего пакета types, с методами и гарантиями. Если настраивать переопределение в каждой записи отдельно, рано или поздно записи разъедутся: одна получит обновление, вторая — забудет.

Для этого у конфига есть верхний этаж — глобальные overrides. Блок overrides размещается на верхнем уровне sqlc.yaml, вне списка sql, и документируется фразой, которая почти дословно описывает наш случай: иногда одна и та же конфигурация нужна сразу нескольким наборам генерации. Внутри — два раздела: rename переименовывает поля структур по имени колонки во всех пакетах разом (классика — id в Identifier), а overrides задаёт отображение типов: db_type указывает тип базы, go_type — куда его спроецировать, вплоть до import, package и type в чужом модуле. У каждого правила глобального блока есть и ключ engine — он нужен только в конфигах, где определения различаются движками; в однодвижковом проекте его можно не писать, значение возьмётся из текущей записи.

Правило выбора этажа простое. Одинаковое для всех пакетов — наверх, в глобальный overrides: один источник, ноль расхождений. Различающееся — внутрь gen.go конкретной записи, где у каждого пакета свои локальные настройки. И аккуратность с go_type: тип из вашего модуля должен быть совместим со сканированием колонки — это уже ваш контракт с драйвером, генератор только вставляет имя.

### Верхний этаж конфига: правила для всех пакетов разом

```go
# sqlc.yaml: глобальные overrides — до списка sql
version: "2"
overrides:
  go:
    rename:
      id: "Identifier"              # каждое поле id во всех пакетах станет Identifier
    overrides:
      - db_type: "pg_catalog.int8"  # BIGINT — суммы в копейках
        engine: "postgresql"
        go_type:
          import: "tracker/internal/types"
          package: "types"
          type: "Money"             # int64 заменяется на types.Money везде
sql:
  # ... две записи пакетов из канона выше — без изменений
```

Блок overrides стоит рядом с sql, на верхнем уровне, и действует на каждую запись списка. rename говорит: колонка id в любом пакете станет полем Identifier. overrides проецирует BIGINT (внутреннее имя pg_catalog.int8) на types.Money из вашего модуля — и tododb, и spendingdb ссылаются на один и тот же тип, никакого дубля смысла. Ключ engine в правиле — атрибут именно глобального этажа: он различает правила в многодвижковых конфигах, а в однодвижковом его можно опустить. После правки — sqlc generate, один запуск, и оба пакета переезжают на Money. Локальные переопределения внутри gen.go никуда не деваются: этажи складываются, локальное уточняет глобальное.

### Проверка: какой этаж конфига

**Вопрос:** Команда решила: поле id во всех структурах всех пакетов должно называться Identifier. Где место этой настройки в sqlc.yaml с двумя записями?

- В gen.go каждой записи: продублировать локальные overrides в обоих пакетах
- ✅ В глобальном блоке overrides на верхнем уровне: go: rename с парой id → Identifier
- В schema.sql: переименовать колонку id в identifier во всех таблицах
- Нигде: имена полей фиксированы генератором и не настраиваются

Настройка, одинаковая для всех наборов генерации, живёт на глобальном этаже: верхнеуровневый overrides с разделом go и rename. Дублирование в каждой записи — ружьё для расхождений: завтра поправят в одной и забудут во второй; локальные места нужны как раз для различающегося. Переименование колонки в базе — другая операция с другой ценой, и её никто не требовал. Имена полей настраиваются штатно — rename существует ровно для этого. В конфиге из двух записей глобальный этаж — единственное место, где правило пишется один раз и применяется к обоим пакетам.

### emit-опции: короткое повторение перед разветвлением

Настройки gen.go, которые второй и четвёртый уроки разбирали по одному, живут на пакет — и теперь это перестаёт быть деталью. Краткий словарь тем урока. emit_json_tags добавляет структурам json-теги: включайте там, где структуры едут в HTTP-ответы, а не во всём подряд. emit_interface печатает интерфейс Querier поверх структуры — граница для подмены реализации; пакет задач с панелью фильтров выигрывает от неё больше, чем журнал. emit_exact_table_names заставляет имена структур точно повторять имена таблиц без единения множественного числа. emit_empty_slices возвращает пустой срез вместо nil из выборок-списков. emit_pointers_for_null_types проецирует nullable-колонки в указатели — для pgx/v5 это текущий канон курса. Здесь важен не словарь, а принцип: значение каждой настройки по умолчанию одинаково во всех пакетах, но включать их можно по-разному — домены настраиваются порознь, и это осознанное решение, а не расхождение версий конфига.

### Два пакета рядом: кто что получает

| Аспект | Пакет tododb | Пакет spendingdb |
| --- | --- | --- |
| Запись в sqlc.yaml | name: todos, queries: db/queries/todos | name: spendings, queries: db/queries/spendings |
| Вход схемы | db/migrations целиком | db/migrations целиком |
| Таблицы в SQL | public: todos, users | billing: spendings |
| package и out | tododb → internal/store/tododb | spendingdb → internal/store/spendingdb |
| Модели | Todo, User, BillingSpending | только затронутое запросами: BillingSpending (omit_unused_structs) |
| Файл моделей | models.go | billing_models.go (output_models_file_name) |

### Восстановите шаг домена бухгалтерии

Три пропуска: создание схемы и два квалифицированных имени — в таблице и в запросе.

```go
-- db/migrations/000004_billing.sql
CREATE ___ billing;                        -- пространство имён домена

CREATE TABLE ___ .spendings (
    id BIGSERIAL PRIMARY KEY,
    todo_id BIGINT NOT NULL REFERENCES todos (id),
    amount BIGINT NOT NULL,                -- сумма в копейках
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- db/queries/spendings/spendings.sql
-- name: CreateSpending :exec
INSERT INTO billing.___ (todo_id, amount) VALUES ($1, $2);
```

Подсказка: Схема создаётся словом CREATE SCHEMA; дальше все ссылки вне public пишутся с префиксом схемы — и в DDL, и в запросах.

Правильные ответы в пропусках: `SCHEMA`, `billing`, `spendings`

### Соберите разветвление трекера на домены

Девять строк: проект с одним пакетом db превращается в два пакета — задачи и бухгалтерия. Шаги идут от папок к конфигу, от генерации к коду приложения.

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

1. `mkdir db/queries/todos db/queries/spendings — домены получают собственные папки`
2. `todos.sql и users.sql переезжают в db/queries/todos, панель фильтров — вместе с ними`
3. `spendings.sql переезжает в db/queries/spendings и получает квалифицированные имена billing.spendings`
4. `пишем миграцию 000004_billing.sql с CREATE SCHEMA billing и таблицей billing.spendings`
5. `добавляем в sqlc.yaml вторую запись sql: schema, queries и gen.go с package: spendingdb`
6. `sqlc generate — один запуск обновляет оба пакета`
7. `в internal/store/spendingdb находим BillingSpending — префикс схемы прижился`
8. `включаем omit_unused_structs: true в spendingdb — чужие модели уезжают из пакета`
9. `переписываем импорты сервиса: tododb и spendingdb вместо одного db`

### Карточки: несколько схем и организация

- **Схема Postgres** — Пространство имён внутри одной базы: таблицы, перечисления, представления, функции. Схема по умолчанию — public. Схема — не база: домены в схемах живут в одной базе и не теряют ссылочную целостность; внешний ключ через схемы законен.
- **CREATE SCHEMA глазами sqlc** — Генератор разбирает CREATE SCHEMA штатно: схема попадает в его каталог. Текущей схемы у sqlc нет и SET он не исполняет — таблицы вне public создаются сразу с префиксом (CREATE TABLE billing.spendings), и в запросах имена пишутся квалифицированно.
- **search_path и sqlc** — В живой базе search_path ищет короткие имена по списку схем; sqlc этого не эмулирует: неквалифицированное имя разрешается только в public, ключа search_path в конфиге нет (вопрос открыт в репозитории), а SET генератор не исполняет. Короткое имя мимо public — падение генерации с ошибкой несуществующей таблицы.
- **Go-имена вне public** — Для схем вне умолчания к имени добавляется префикс: billing.spendings → структура BillingSpending, billing.payment_status → BillingPaymentStatus с константами вида BillingPaymentStatusCaptured. В public — без префикса, как канонический Todo.
- **sqlc.yaml multiple packages** — Ключ sql — список: каждая запись несёт name (необязательный человекочитаемый идентификатор), engine, schema, queries (папка или список путей) и gen.go с package и out. Разные out — разные Go-пакеты; один sqlc generate обходит все записи за один запуск; движки в соседних записях могут различаться.
- **Модели и общие типы** — Каждый пакет печатает models.go по всему входу схемы — дубль лечится omit_unused_structs: true. Имя файла моделей меняет output_models_file_name (по умолчанию models.go). Одинаковые для всех пакетов правила — глобальные overrides на верхнем уровне: go.rename для полей и go_type с import/package/type для общих типов.

### Что дальше

Трекер разветвился по всем этажам сразу. В базе журнал списаний получил собственную схему billing, и SQL-запросы читаются как объявления домена. В конфиге ключ sql стал списком из двух записей: schema у них общая — папка миграций остаётся единственной правдой о структуре, — queries делятся по доменам, а gen.go выдаёт два независимых пакета с собственными моделями и интерфейсами. Дубль моделей усекается omit_unused_structs, имена файлов настраиваются, а смыслы, общие для доменов, живут на верхнем этаже глобальных overrides. Так и собирается организация sqlc: граница домена — в схеме, в папке queries и в импорте пакета одновременно.

Следующий шаг — другой движок: восемнадцатый урок расскажет про MySQL — чем отличается синтаксис, как меняются типы и что из привычных конструкций не переносится; конструкция «две записи в одном конфиге» — та самая дверь, в которую MySQL входит без переписывания проекта. Конфиг с путями и базовыми настройками — во [втором уроке](/courses/sqlc/lessons/02-nastrojka/); папка миграций как источник схемы — в [шестнадцатом](/courses/sqlc/lessons/16-migracii/); план остальных тем — на странице [курса sqlc](/courses/sqlc/).

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

- Схема Postgres — пространство имён внутри одной базы; умолчание — public. Домен в собственной схеме даёт границы в SQL, права на уровне схемы и свободный словарь имён; внешний ключ через схемы законен.
- sqlc разбирает CREATE SCHEMA, но search_path не эмулирует: неквалифицированные имена ищутся только в public, ключа настройки нет, SET не исполняется. Всё вне public — квалифицированные имена в DDL и запросах.
- Таблицам и перечислениям вне public генератор добавляет префикс схемы: billing.spendings → BillingSpending, billing.payment_status → BillingPaymentStatus. В public — прежние простые имена.
- sqlc.yaml multiple: ключ sql — список, каждая запись — name, engine, schema, queries и gen.go со своим package/out. Один sqlc generate обходит все записи; на домен — свой Go-пакет с собственными моделями и Querier.
- Разделение доменов: один домен — папка queries и запись конфига; запрос на стыке принадлежит домену главного данных; транзакционные сценарии через два домена не ломаются — пакеты делят методы, а не соединения.
- Модели строятся по всему входу схемы обоими пакетами: дубль усекается omit_unused_structs, имя файла меняет output_models_file_name; одинаковое для всех пакетов — в глобальные overrides (go.rename, go_type с import), различное — в локальные настройки gen.go.
- emit-настройки живут на пакет и включаются порознь: emit_json_tags, emit_interface, emit_exact_table_names, emit_empty_slices, emit_pointers_for_null_types — словарь из второго и четвёртого уроков, применяемый на границах домена.

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

Разведите трекер на домены. Первое: примените к локальной базе шаг 000004_billing.sql — CREATE SCHEMA billing и таблицу billing.spendings со внешним ключом на todos; убедитесь в psql командой \dn, что схема появилась, и что INSERT в billing.spendings со ссылками на реальные задачи проходит. Второе: попробуйте создать таблицу без префикса сразу после CREATE SCHEMA и найдите, в какой схеме она оказалась; объясните письменно, почему sqlc не увидел бы её в billing. Третье: сломайте генерацию коротким именем — напишите SELECT * FROM spendings без квалификатора и прочитайте ошибку несуществующей таблицы; почините до FROM billing.spendings. Четвёртое: создайте папки db/queries/todos и db/queries/spendings, разнесите файлы по доменам и соберите канонический конфиг урока с двумя записями; запустите sqlc generate один раз и проверьте, что оба каталога internal/store обновились. Пятое: найдите в spendingdb структуру BillingSpending и объясните себе префикс по формуле схема_таблица; создайте в схеме billing перечисление payment_status и сверните его в BillingPaymentStatus. Шестое: включите omit_unused_structs в spendingdb, перегенерируйте и убедитесь, что Todo исчез из бухгалтерского пакета, а CreateSpendingParams остался; затем переименуйте файл моделей через output_models_file_name. Седьмое: вынесите общий тип Money в internal/types и подключите его глобальным overrides с db_type: pg_catalog.int8; проверьте, что поле Amount в обоих пакетах стало Money после одного запуска генерации. Восьмое: перепишите сводку Dashboard из урока на два импорта и два объекта Queries; опишите в README проекта, где живут границы доменов — в схеме, в папке queries или в импорте, и почему все три ответа правильные.

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

### Почему структура называется BillingSpending, а не просто Spending?

Для таблиц вне схемы по умолчанию генератор строит имя по шаблону схема_таблица и приводит его к Go-стилю: billing_spendings → BillingSpending. Префикс сохраняет связь с квалифицированными именами SQL — в коде так же видно, чья таблица, как и в запросе. Настройки, отключающей префикс, нет: в схеме public префикс просто не нужен, там генерация остаётся прежней.

### Как sqlc разрешает короткие имена таблиц, если схем несколько?

Никакой search_path-логики у генератора нет: неквалифицированное имя ищется только в схеме по умолчанию — public. Ключа search_path в конфиге не существует, а оператор SET он не исполняет. Если таблица живёт в другой схеме, ссылайтесь на неё квалифицированно — billing.spendings; короткое имя мимо public упадёт с ошибкой несуществующей таблицы на этапе генерации.

### Стоит ли делить проект на пакеты, если таблиц пока мало?

Не обязательно: один пакет с файлами по темам — честный формат небольшого проекта. Пакеты стоит заводить, когда появился второй домен с собственными правилами жизни: свои права в базе, свой темп изменений, своя команда. Разветвление обратимо и дёшево: папки queries, записи в списке sql, один запуск генерации — а граница, которую охраняет компилятор, окупается с первого конфликта на ревью.
