приём и чтение записей сведены к одному контракту приложения
- адреса приложения переехали в своё пространство `/app/`, опрос готовности убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи, текст — отдельным адресом названного вида - заведена единая точка отображения доменной ошибки и слой, приводящий к той же форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением - у записи появились имя файла отправителя, длительность и размер своими колонками, а у ленты владельца — свой индекс: без него страница сканировала весь архив сервиса
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
|
||||
"git.vakhrushev.me/av/transcriber/internal/entity"
|
||||
)
|
||||
|
||||
// up202608150001 заводит у аудиозаписи три колонки, которые показывает список
|
||||
// приложения: имя файла, данное отправителем, длительность и размер принятого.
|
||||
//
|
||||
// Шаг один на все три намеренно. Применённый шаг не переписывается, и три шага
|
||||
// вместо одного стоили бы трёх необратимых решений там, где хватает одного.
|
||||
//
|
||||
// **Имя файла ложится своей колонкой, а не в заголовок.** Заголовок несёт
|
||||
// название, которое дал человек либо посчитала языковая модель; имя файла — то,
|
||||
// по чему человек узнаёт свою запись, пока заголовка нет. Одной колонкой на оба
|
||||
// смысла посчитанное название затирало бы имя, и вернуть затёртое было бы
|
||||
// неоткуда.
|
||||
//
|
||||
// **Длительность и размер дублируют строку файла, и это решение владельца от
|
||||
// 2026-08-15.** Равенство между ними не поддерживается никем: на записи лежит
|
||||
// снимок принятого, взятый приёмом один раз, на файле — величины той копии,
|
||||
// которой файл является сейчас. Расхождение — не поломка, а разные вопросы;
|
||||
// норму держит capability `storage`.
|
||||
//
|
||||
// Единица стоит в имени колонки, а не в комментарии: расхождение «секунды против
|
||||
// миллисекунд» между колонкой, ответом списка и объявленным пределом не увидит
|
||||
// ни компилятор, ни гейт — оба конца числа.
|
||||
func up202608150001(app core.App) error {
|
||||
records, err := app.FindCollectionByNameOrId(RecordsCollection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find collection %s: %w", RecordsCollection, err)
|
||||
}
|
||||
|
||||
records.Fields.Add(
|
||||
// Имя приходит извне и содержимым своим приёму не подконтрольно, поэтому
|
||||
// приём режет его по пределу и убирает управляющие знаки прежде, чем
|
||||
// сохранить. Схема держит потолок вторым рубежом: значение сверх него
|
||||
// отвергается хранилищем, а не доезжает до экрана.
|
||||
&core.TextField{Name: "original_filename", Max: entity.MaxOriginalFilenameLen},
|
||||
// Длительность и размер принятого. «Неизвестно» колонки не выражают:
|
||||
// числовая колонка хранилища пустого значения не держит, пустое кладётся
|
||||
// нулём. Обе ставит приём и ставит всегда — запись с непрочитанными
|
||||
// метаданными отвергается отказом и не заводится. Решение владельца
|
||||
// 2026-08-15.
|
||||
&core.NumberField{Name: "duration_ms", OnlyInt: true, Min: ptr(0.0)},
|
||||
&core.NumberField{Name: "size_bytes", OnlyInt: true, Min: ptr(0.0)},
|
||||
)
|
||||
|
||||
// Индекс под ленту приложения. Единственный прежний индекс — по рубежу и
|
||||
// признаку остановки — заведён под захват воркера и выборке владельца не
|
||||
// помогает ничем: страница сканирует таблицу целиком и досортировывает
|
||||
// результат во временном дереве.
|
||||
//
|
||||
// Замер на этом же изменении: рост архива с 5 тысяч строк до 200 тысяч —
|
||||
// сорокакратный — растит время одной страницы владельца в двадцать-тридцать
|
||||
// раз, хотя записей у него всё те же сорок. Цена растёт с **чужими**
|
||||
// записями, потому что сервис объявлен архивом и хранит их бессрочно.
|
||||
//
|
||||
// Порядок колонок повторяет порядок выборки: сужение по владельцу, затем
|
||||
// сортировка «новыми сверху» полным ключом.
|
||||
records.AddIndex("idx_audio_records_owner_feed", false, "owner, created DESC, id DESC", "")
|
||||
// Отбор тремя состояниями сужает по владельцу вместе с рубежом и признаком
|
||||
// остановки — своим индексом, потому что ведущей колонкой здесь владелец.
|
||||
records.AddIndex("idx_audio_records_owner_state", false, "owner, state, halted_at", "")
|
||||
|
||||
if err := app.Save(records); err != nil {
|
||||
return fmt.Errorf("failed to add contract columns to %s: %w", RecordsCollection, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// down202608150001 снимает три колонки. Данные в них при этом теряются, и
|
||||
// восстановить их неоткуда: имя файла отправителя нигде больше не хранится.
|
||||
func down202608150001(app core.App) error {
|
||||
records, err := app.FindCollectionByNameOrId(RecordsCollection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find collection %s: %w", RecordsCollection, err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"original_filename", "duration_ms", "size_bytes"} {
|
||||
records.Fields.RemoveByName(name)
|
||||
}
|
||||
records.RemoveIndex("idx_audio_records_owner_feed")
|
||||
records.RemoveIndex("idx_audio_records_owner_state")
|
||||
|
||||
if err := app.Save(records); err != nil {
|
||||
return fmt.Errorf("failed to drop contract columns from %s: %w", RecordsCollection, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -50,6 +50,7 @@ func init() {
|
||||
pbmigrations.Register(up202608140001, down202608140001, "202608140001_record_owner.go")
|
||||
pbmigrations.Register(up202608140002, down202608140002, "202608140002_record_centric_model.go")
|
||||
pbmigrations.Register(up202608140003, down202608140003, "202608140003_owner_required.go")
|
||||
pbmigrations.Register(up202608150001, down202608150001, "202608150001_record_contract_columns.go")
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
Reference in New Issue
Block a user