migrations

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

README

kaname — миграции

Schema kaname. Число миграций здесь НЕ выписано намеренно: оно росло с каждой задачей, а строка с ним не правилась ни разу — на момент этой правки документ утверждал двадцать четыре при ста семнадцати файлах в каталоге. Перечень получают предикатом, а не памятью:

ls internal/migrations/*.sql | wc -l

[!warning] Предикат выше вёл по пути services/iam/internal/migrations, где НОЛЬ файлов Служба вынесена отдельным продуктом, и корнем стал сам репозиторий. Команда, выданная читателю, отвечала «нет такого каталога»: документ, специально написанный против устаревания ЧИСЛА, выдал устаревшую КООРДИНАТУ (kacho#2480).

Эры номеров здесь НЕ перечисляются: перечень менялся молча вместе с каталогом, а переномеровать применённое нельзя, поэтому выписанный перечень стареет сам. Прежняя редакция называла три эры и приводила номера, которых в каталоге нет ни одного. Действующую форму объявляет раздел «Форма НОВОЙ миграции» ниже, а что лежит сегодня — тот же предикат, что и счёт.

Форма НОВОЙ миграции — метка времени заведения YYYYMMDDHHMMSS_<имя>.sql (date -u +%Y%m%d%H%M%S). Объявлена она в ОДНОМ месте и здесь НЕ переписывается: docs/engineering/architecture/migration-version-namespace.md — там же названо, чем закрыты доводы прежней формы и что делать, когда часы дали метку ниже уже лежащей. Держит форму гейт internal/check TestNewMigrationOutranksEveryAppliedOne (internal/check/migration_version_monotonic_test.go); он судит ТОЛЬКО добавленные относительно ствола файлы. Создают миграции этого каталога:

  • core resource model (operations, users, accounts, projects, service_accounts, groups, group_members, roles, access_bindings)

    • default-roles seed (12 system-ролей с детерминированными id);
  • governance расширения (access_bindings_jit_eligibility, _jit_pending, break_glass_*);

  • federation / identity (federation_*, scim_*);

  • чеканку своих токенов — ключница подписных ключей (token_signing_keys: kid, алгоритм, состояние, публичная половина, ОБЁРНУТАЯ приватная половина, срок; «подписывает ровно один» держит частичный уникальный индекс) и отзыв отчеканенного (minted_token_revocations);

  • способ входа человека — строка на пару «человек, вид» с проверочным материалом (user_login_methods; материал — секрет: копий и механизмов, исполняющихся при записи без ведома писателя, нет) и подтверждённость адреса рядом с самим адресом (users.email_verified_at, снимается базой при смене адреса) — фаза Ф2, kacho#1268;

  • outbox-cluster. Перечень очередей здесь НЕ выписывается: он менялся молча — на момент этой правки в нём стояла очередь, которой миграции не заводят вовсе. Действующие получают предикатом:

    grep -ho '[a-z_]*_outbox' internal/migrations/*.sql | sort -u
    

[!note] Прежнее хранилище подписных ключей здесь названо не было, и это не опечатка редакции Оно существовало, стояло в этом перечне и было СНЯТО миграцией: приватную половину не читал обратно никто, а запись в хранилище удавалась — поэтому оно выглядело исправным всё время своей жизни. Имя снятой таблицы в этом перечне не воспроизводится: в перечне действующих таблиц оно читается как действующая координата. Его текст остался в применённых миграциях и в ведомости дропов — там он свидетельство, и править его нельзя.

Ключница задачи kacho#897 названа ИНАЧЕ намеренно. Совпадение имён сделало бы стража ведомости дропов красным СПРАВЕДЛИВО: он увидел бы воскресшую таблицу, которую сам же считает снятой.

[!warning] Гейт формы здесь назывался координатой ЧУЖОГО репозитория Прежняя редакция называла держателем PRO-Robotech/kacho:internal/repohygiene TestNewMigrationOutranksEveryAppliedOne. Гейт с этим именем существует, красным бывает и судит каталоги МОНОРЕПО: git ls-files -- '*/migrations/*.sql' в клоне PRO-Robotech/kacho даёт 210 файлов и ноль путей этой службы. Утверждение было ложным не по тексту, а по ОБЛАСТИ — тот же класс, что и координата предиката выше (kacho#2480). Гейт заведён здесь и судит этот каталог; исторический остаток он печатает числом, потому что применённую миграцию не переименовывают (запрет #5).

CHECK / FK / UNIQUE / partial UNIQUE / триггеры inline в соответствующих миграциях. Helper-функции: labels_valid(jsonb) (в своде — kacho_labels_valid, переименована миграцией 20260917130000), iam_permissions_valid(jsonb), триггер group_members_member_exists_trg.

Squash в единый 0001_initial.sql baseline запланирован в Wave M рефакторинга KAC-193 (после Wave D evgeniy-compliance pass).

Запуск

make migrate-up (требует KANAME_DB_PASSWORD=...).

Запреты

  • НЕ редактировать примененную миграцию (workspace CLAUDE.md запрет #5) — только новая миграция, и её номер — метка времени заведения (YYYYMMDDHHMMSS_<имя>.sql), а не следующий по порядку. Здесь стояло «следующий — 0025_*»: номер выписан, ни разу не правился и разошёлся с деревом на порядки (приводимых там номеров в каталоге нет ни одного), а инкрементная форма к тому же отвергается гейтом формы. Каталог не пересчитывают глазами — предикат выше.
  • Таблица operations объявлена в baseline под схемой kaname. Цели, которая «подтягивала бы общую operations», больше нет: она ничего не делала ни в одном сервисе, а копия common-файла создала бы конфликтующий unqualified public.operations.

Documentation

Overview

grant_removal_trace.go — миграция, СНИМАЮЩАЯ выдачи, обязана оставить след той формы, которую платформа для этого завела; и ни одна миграция не переносит выдачу с одной роли на другую.

Порт с монорепо (`internal/repohygiene/grantremovaltrace.go`, снят вынесением службы — `kacho#2597`; приёмка `seed-identity-names-its-own-service.md` §6 уже называла держателем `TestNoMigrationMovesGrantsBetweenRoles`, а он не существовал ни в одном файле — только в комментариях).

Предмет

Строка `kaname.access_bindings` — это доступ, выданный арендатору. Миграция, удаляющая её, доступ ОТБИРАЕТ; и если рядом нет записи в журнал жизненного цикла выдачи, у которого есть читатель, то «отобрано» становится неотличимо от «никогда не выдавалось». Арендатор видит отсутствие права и не может узнать ни что оно было, ни когда исчезло, ни почему.

Замер на дне переезда (эта ревизия, `internal/migrations`)

Корпус — 10 файлов. Ровно один называет таблицу выдач ОПЕРАТОРОМ удаления (`20260909202745_module_identities_leave_the_baseline.sql`), и он несёт след — вставку в `kaname.audit_outbox` — ДО удаления, в той же накатной половине. Храповик заведён нулём и это подтверждено переписью гейта, а не предположено: см. `TestMigrationRemovingGrantsLeavesATrace`.

Почему ХРАПОВИК, а не ведомость имён

Применённую миграцию не правят (запрет #5) — значит найденную без следа придётся либо простить, либо держать ствол красным за прошлое. Ведомость имён растёт молча, и её никто не сверяет; число сверяется одним обходом и меняется только сознательно. Перечень имён при этом ПЕЧАТАЕТСЯ каждым прогоном — прощение остаётся видимым.

Храповик ДВУСТОРОННИЙ: находка приходит и на следующей миграции без следа, и на уменьшении числа прощённых. Односторонний потолок не истекает никогда и потому механизмом не является.

Почему снятие выдач У СУБЪЕКТА входит в класс

Норма — про ОТОБРАННЫЙ ДОСТУП, а не про снятую роль: строки выдач исчезают одинаково, а служебная учётка — такой же первоклассный принципал, как человек.

Граница, названная честно

Гейт судит НАКАТНУЮ половину: `DELETE` в откатной половине снимает СВОЮ ЖЕ вставку, откатываясь, и доступа не отбирает. Он не судит, ВЕРЕН ли текст оставленного следа и доедет ли он до читателя, — только что запись есть. И он ничего не говорит о выдачах, снятых прод-кодом: у того путь свой и свой журнал.

person_and_account_keys.go — каждый внешний ключ на `users` и `accounts` записан в ведомости вместе с действием и доводом (задача kaname#664, приёмка `own-account-and-identity-are-removed-in-order.md`, Р9, KN664-60/61).

Предмет

Арендатор не мог уйти, потому что два ключа на `users` и `accounts` образовали цикл `RESTRICT`, а ещё три `RESTRICT` на `users` стояли на том же пути. Ни один из них не был ошибкой по отдельности — каждый заводился своей задачей без вопроса «как это снимается». Цикл вырос молча и молча вырастет снова, если новый ключ на эти таблицы не обязан называть своё действие.

Почему судится НАКАТАННАЯ схема, а не текст миграций

Текстовая перепись не видит снятия таблицы вместе с её ключом и не различает ключ, объявленный в `CREATE TABLE`, и ключ `ADD CONSTRAINT` иначе, как разбором SQL. Каталог базы отвечает на сам вопрос: какие ключи на эти таблицы существуют после наката всей цепи. Поэтому держатель — интеграционная проба этого пакета, а судья ниже — чистая функция над тем, что она прочла.

Ведомость истекает сама

Строка ведомости без ключа — находка, а не молчание: снятый ключ, о котором ведомость продолжает говорить, есть то же «два места об одном предмете».

sql_text.go — исполняемая часть миграции: Up-секция без комментариев и без содержимого строковых литералов.

Порт с монорепо (`internal/repohygiene/migrationsqltext.go`, снят вынесением службы — `kacho#2597`). Поведение переносом не менялось: функции перенесены дословно и экспортированы, чтобы их звала внешняя тестовая обёртка (`package migrations_test`), а не только пакет-владелец.

ЗДЕСЬ СТОЯЛО «в этом дереве сегодня один владелец — `grant_removal_trace.go`», и предостережение «заводя второй, проверь, не завёл ли кто-то копию». Оба пережили свой предмет, и второе не сработало НИ РАЗУ — именно потому, что первое лгало: читатель, которому сказали «владелец один», копию не искал.

Перемер 2026-09-14. ЕДИНИЦА СЧЁТА НАЗВАНА, потому что два образца об одном предмете дают разные числа: «файл называет имя» считает и прозу, и текст находки нового гейта (13), «файл ЗОВЁТ» считает вызовы (11). Ниже — второе.

git grep -lE '(MigrationUpSection|MigrationUpText)\(' -- '*.go' \
  | grep -v internal/migrations/sql_text.go       -> 11 файлов, из них 4 не-тестовых
git grep -ln 'strings.Index([a-z]*, *"-\{0,2\} *+goose' -- '*.go'  -> 8, все _test.go

До сведения СВОИХ разрезов в дереве было одиннадцать, и они давали ПЯТЬ разных ответов на вопрос «что делать с файлом без маркера прямого хода»: отдать тело ЦЕЛИКОМ вместе с откатом · отдать пустую строку · отдать всё от начала · отказать · не спрашивать вовсе. Две копии жили в не-тестовом коде гейтов и сведены сюда (`kacho#2363`); восемь остались в пробах, где директива есть СОДЕРЖИМОЕ фикстуры, и это названная граница, а не чистота.

ДЕРЖИТСЯ ГЕЙТОМ, А НЕ ЭТОЙ ФРАЗОЙ: `internal/check` — `TestMigrationSectionSplitIsDeclaredOnce` требует, чтобы директива секции была выписана литералом только здесь, печатает объём осмотренного и падает на пустом обходе. Предостережение без держателя — то, что здесь уже стояло.

Index

Constants

View Source
const (
	// SectionMarkerStem — общее начало обеих директив секции.
	SectionMarkerStem = "-- +goose "
	// SectionDownMarker — начало ОБРАТНОГО хода миграции.
	SectionDownMarker = SectionMarkerStem + "Down"
)

Директивы секции мигратора. ЛИТЕРАЛ ЖИВЁТ ЗДЕСЬ И БОЛЬШЕ НИГДЕ: выписанный второй раз, он превращается во второе место об одном предмете — и расходится молча, потому что на законном входе обе записи отвечают одинаково.

Стем объявлен отдельно, а маркер отката из него СОБРАН, чтобы у написания была одна точка правки: в этом дереве уже встречались три написания одного маркера (`-- +goose Down`, `+goose Down` без приставки и пара `Up`+`Down` с разрезом между ними), и различались они ровно тем, чего никто не решал.

View Source
const GrantRemovalRatchet = 0

GrantRemovalRatchet — сколько миграций корпуса СЕГОДНЯ снимают выдачи без следа. Число, а не перечень: см. шапку файла. Отклонение в любую сторону — находка.

View Source
const SessionTable = "human_sessions"

SessionTable — имя таблицы сессии человека без схемы. Одно объявление на пакет; адаптер называет ту же таблицу своим оператором.

Variables

View Source
var ErrNoKeysWalked = fmt.Errorf("обход накатанной схемы не нашёл ни одного внешнего ключа на users и accounts — " +
	"вердикт беспредметен: «ноль находок» неотличимо от «ноль прочитанного»")

ErrNoKeysWalked — обход каталога не нашёл ни одного ключа: вердикта нет.

FS — embedded миграции (squashed baseline в одном файле). Используется cmd/migrator/main.go через runner.Config.FS.

View Source
var PersonAndAccountKeysLedger []byte

PersonAndAccountKeysLedger — ведомость ключей на `users` и `accounts`.

Functions

func AuditGrantRoleReassignment added in v0.2.0

func AuditGrantRoleReassignment(corpus []GrantMigrationSource) (moves []string, statements int)

AuditGrantRoleReassignment — миграции, переставляющие выдачу с одной роли на другую, и число осмотренных операторов.

Второе число обязательно: ноль переносов при нуле прочитанных операторов означает «распознаватель ослеп», а не «переносов нет».

func BaselineVersion added in v0.5.0

func BaselineVersion(fsys fs.FS) (int64, error)

BaselineVersion — версия СВОДА цепочки: наименьшая версия среди её миграций.

ВЫВОДИТСЯ ИЗ ЦЕПОЧКИ, А НЕ ВЫПИСЫВАЕТСЯ ЧИСЛОМ. Выписанное число не имеет производителя: сведение цепочки заново (а оно уже было — 171 миграция сведена в один файл) сдвинуло бы свод, и константа рядом осталась бы прежней молча.

Пустой обход — ОТКАЗ, а не ноль: «версий не нашлось» иначе неотличимо от «версий нет», и страж, который на этом строится, промолчал бы там, где ему полагается отказать.

func ExpiryDeclarationsIn added in v0.5.0

func ExpiryDeclarationsIn(bodies map[string]string) ([]ExpiryDeclaration, ExpiryCensus)

ExpiryDeclarationsIn — объявления срока у таблицы сессии по телам миграций (имя файла → текст). Тело режется на секцию наката и забеливается от комментариев ЗДЕСЬ, а не вызывающим: иначе два вызывающих разошлись бы в том, что считать исполняемым.

func ExpiryDeclarationsInFS added in v0.5.0

func ExpiryDeclarationsInFS(fsys fs.FS) ([]ExpiryDeclaration, ExpiryCensus, error)

ExpiryDeclarationsInFS — то же по встроенному каталогу миграций.

func GrantRemovalFinding added in v0.2.0

func GrantRemovalFinding(got int, names []string) string

GrantRemovalFinding — текст находки. Отдельной функцией, чтобы обе стороны храповика (перебор и недобор) говорили ОДНИМ голосом и назывались вместе с перечнем.

func GrantTableMentions added in v0.2.0

func GrantTableMentions(corpus []GrantMigrationSource) int

GrantTableMentions — сколько операторов корпуса называют таблицу выдач ЛЮБЫМ глаголом. Общая предпосылка обоих гейтов файла: ноль означает, что корпус не прочитан либо таблица выдач из него исчезла.

func JudgePersonAndAccountKeys added in v0.5.0

func JudgePersonAndAccountKeys(found []ForeignKeyOnPersonOrAccount, ledger []KeyLedgerRow) ([]string, error)

JudgePersonAndAccountKeys сверяет ключи каталога с ведомостью. Находки — по одной строке на нарушение, отсортированы; пустой обход — ошибка.

func MigrationUpSection added in v0.2.0

func MigrationUpSection(body string) string

MigrationUpSection — исполняемая часть миграции: Up-секция без комментариев.

Порядок двух шагов обязателен и обратим быть не может: маркер отката САМ является комментарием, поэтому секция отрезается по СЫРОМУ тексту, и только потом забеливаются комментарии. Забелив сперва, разбор потерял бы границу секций и прочитал бы откат как часть наката.

func MigrationUpText added in v0.5.0

func MigrationUpText(body string) string

MigrationUpText — ПРЯМОЙ ход миграции сырым текстом, без обработки комментариев.

Отделён от MigrationUpSection потому, что у разреза и у снятия комментариев РАЗНЫЕ вызывающие: часть гейтов дерева снимает комментарии своим разбором и нуждается только в разрезе. Пока отдельного входа не было, каждый такой гейт заводил СВОЙ разрез — и они разошлись в том, что делать с файлом без маркера прямого хода: один отдавал тело целиком ВМЕСТЕ С ОТКАТОМ, другой пустую строку, третий отказывал.

Здесь правило одно и оно названо: маркера отката нет — тело прямого хода есть весь текст; есть — прямой ход кончается на нём. Маркер ПРЯМОГО хода в разрезе не участвует вовсе: всё, что стоит до него, — шапка файла, то есть комментарий, и исполняемого в ней нет by construction.

func OnDeleteWord added in v0.5.0

func OnDeleteWord(code string) (string, error)

OnDeleteWord — слово действия по коду каталога (`pg_constraint.confdeltype`). Неизвестный код — отказ, а не пустое слово: пустое совпало бы с пустым полем ведомости.

func SQLBlankComments added in v0.2.0

func SQLBlankComments(s string) string

SQLBlankComments заменяет содержимое SQL-комментариев пробелами, сохраняя длину строки и позиции переводов строк.

Приоритет состояний: строка > комментарий > код. Внутри строкового литерала `--` комментария не начинает; внутри комментария кавычка строки не открывает. Блочные комментарии Postgres ВЛОЖЕННЫЕ — глубина считается, а не ищется первый `*/`.

func SQLBlankStrings added in v0.2.0

func SQLBlankStrings(s string) string

SQLBlankStrings заменяет СОДЕРЖИМОЕ строковых литералов пробелами, сохраняя длину и позиции переводов строк.

Зачем отдельно от SQLBlankComments. Тот забеливает комментарии и при этом НАМЕРЕННО пропускает литералы целиком — иначе `--` внутри строки открыл бы комментарий. Но литерал не исполняется как SQL: прозаическое сообщение об ошибке, содержащее слова конструкции, для инвентаря по образцу неотличимо от самой конструкции.

Кавычки-границы ОСТАЮТСЯ на месте: забеливается содержимое, поэтому `WHERE x = 'DROP COLUMN'` остаётся синтаксически тем же и по-прежнему не склеивает соседние лексемы.

Types

type ExpiryCensus added in v0.5.0

type ExpiryCensus struct {
	Files          int // файлов миграций прочитано
	TableMentions  int // объявлений/изменений таблицы сессии найдено
	ColumnsSeen    int // столбцов таблицы сессии осмотрено
	MomentColumns  int // из них моментов, сроком не являющихся
	ExpiryDeclared int // объявлений срока
}

ExpiryCensus — перепись обхода: печатается всегда, чтобы «ноль находок» было отличимо от «ноль прочитанного».

func (ExpiryCensus) String added in v0.5.0

func (c ExpiryCensus) String() string

type ExpiryDeclaration added in v0.5.0

type ExpiryDeclaration struct {
	File   string
	Column string
	Form   string // "create-table" | "add-column"
	Why    string // чем распознано
}

ExpiryDeclaration — найденное объявление срока.

type ForeignKeyOnPersonOrAccount added in v0.5.0

type ForeignKeyOnPersonOrAccount struct {
	Name       string `json:"name"`
	Table      string `json:"table"`
	References string `json:"references"`
	// OnDelete — действие словом каталога: CASCADE, RESTRICT, SET NULL,
	// NO ACTION, SET DEFAULT.
	OnDelete string `json:"on_delete"`
}

ForeignKeyOnPersonOrAccount — внешний ключ, прочитанный из каталога накатанной схемы.

type GrantMigrationSource added in v0.2.0

type GrantMigrationSource struct {
	Name string
	Body string
}

GrantMigrationSource — один файл корпуса: имя для находки и текст для разбора.

func ReadMigrationCorpus added in v0.2.0

func ReadMigrationCorpus(paths []string, read func(string) ([]byte, error)) ([]GrantMigrationSource, error)

ReadMigrationCorpus — корпус миграций, состав берётся у вызывающего (индекс git либо `embed.FS`), а не с диска напрямую.

type GrantRemovalCensus added in v0.2.0

type GrantRemovalCensus struct {
	FilesRead  int
	WithDelete int // удаление выдач где угодно в файле
	InUpHalf   int // из них — в накатной половине
}

GrantRemovalCensus — объём осмотренного. Три величины, а не одна: «ноль находок» обязано быть отличимо от «ноль прочитанного», а «ноль в накатной половине» — от «распознаватель перестал узнавать удаление вовсе».

func AuditGrantRemovalTrace added in v0.2.0

func AuditGrantRemovalTrace(corpus []GrantMigrationSource) (silent []string, c GrantRemovalCensus)

AuditGrantRemovalTrace — ядро гейта. Возвращает имена миграций, снимающих выдачи без следа, и перепись.

type KeyLedgerRow added in v0.5.0

type KeyLedgerRow struct {
	ForeignKeyOnPersonOrAccount
	Reason string `json:"reason"`
}

KeyLedgerRow — строка ведомости: ключ, его действие и довод.

func ParseKeyLedger added in v0.5.0

func ParseKeyLedger(raw []byte) ([]KeyLedgerRow, error)

ParseKeyLedger разбирает ведомость. Неизвестное поле — отказ: строка с опечаткой в имени поля иначе читалась бы как строка без довода.

Jump to

Keyboard shortcuts

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