Костыли ИИ Велосипеды

SG Buddy. Инструмент для формирования запросов для SQLC и proto-файлов через Web User Interface

Что делает программа

Генерирует SQL для SQLC в диалекте PostgreSQL.
Генерирует proto-файлы для Go.
Скачать SG Buddy. Инструмент для формирования запросов для SQLC и proto-файлов через Web User Interface

В чем суть генераторов кода?

Первое мое утверждение: генераторы — это здорово.

Кодогенерация вроде sqlc и protobuf решает одну и ту же фундаментальную проблему: у системы есть контракт — схема базы или описание API, — и есть код, который этому контракту должен соответствовать. Пока соответствие поддерживается руками, оно неизбежно расходится.

Уменьшается механическая работа, и ускоряется рабочий процесс.

Вы создаете контракт, дальше система сама делает все остальное.

Но в этом тоже есть проблемы. Вы пишите первую часть приложения у вас 27 таблиц, где необходимо сделать Create, Rate, Update, Delete для всех таблиц. Задача несложная но писать контракты долго и муторно.

Именно поэтому я решил сделать юзер интерфейс для формирования контрактов.

Что делает инструмент SG Buddy?

Инструмент позволяет делать простые CRUD запросы из визуального интерфейса без написания SQL-кода. Причем я постарался сделать достаточно гибкое решение. Если нужно добавить какой-то сложный кастомный SQL-код, то есть возможность просто его ввести формате SQLC и он добавится в запрос.

Проще всего это сделать на примере.

Мы возьмем несколько таблиц и сгенерируем из них SQL-скрипты и proto-файл.

Единственная вещь, которую нужно сделать до того, как начать работу: подготовить DDL для нужных таблиц.

Для примера нам хватит всего двух таблиц.

sql
CREATE TABLE dc.table_meta (  
    id bigint NOT NULL,  
    name character varying(128) NOT NULL,  
    description character varying(2000) NOT NULL,  
    schema_id bigint NOT NULL,  
    table_type_id bigint NOT NULL,  
    domain_id bigint NOT NULL,  
    is_deleted boolean DEFAULT false NOT NULL,  
    created_at timestamp without time zone DEFAULT now() NOT NULL,  
    updated_at timestamp without time zone DEFAULT now() NOT NULL,  
    is_get_dict bool DEFAULT false NOT NULL,  
    user_id bigint NOT NULL  
);  
CREATE TABLE dc.table_type (  
    id bigint NOT NULL,  
    name character varying(128) NOT NULL,  
    description character varying(1000) NOT NULL,  
    is_deleted boolean DEFAULT false NOT NULL,  
    created_at timestamp without time zone DEFAULT now() NOT NULL,  
    updated_at timestamp without time zone DEFAULT now() NOT NULL,  
    user_id bigint NOT NULL  
);

Именно это вы найдете в \test_data\tables_model\sqchema.sql
И тесты алгоритма будут настроены именно на этот файл

Второй экран

1.1. Названия таблиц
1.2. Кнопки, переключающие генерируемые данные

Create

Это черновик одного INSERT запроса для выбранной таблицы. Живёт до нажатия «Добавить», сбрасывается при смене таблицы.

Поля формы

Поле Что делает
Название Имя запроса = имя метода sqlc. Обязательное, уникальное по всему schema.json (не только по таблице). Рядом кнопка‑подсказка → Предложить название (2.1) Create<CamelTable>.
Query Annotation Выпадающий список: one или exec. one добавляет RETURNING *, exec — нет.
Поля Таблица из трёх колонок по каждой колонке таблицы: имя колонки + её SQL‑тип, поле «замена значений», чекбокс «исключить из генератора».
Custom Query Свободный текст. Если заполнен — отменяет всё остальное (колонки, значения), автор пишет запрос сам. Существует ради ON CONFLICT ....

Логика колонок

  • Значения по умолчанию в «замене» подставляются из DEFAULT в DDL
  • Пустое поле замены = «приходит параметром» → в SQL уйдёт @имя_колонки. Это единственное место в проекте, где пустое поле не превращается в null.
  • Заполненное поле = literal/выражение подставляется как есть (now(), nextval(...), число).
  • Чекбокс «исключить» убирает колонку из INSERT целиком — не попадёт ни в список колонок, ни в VALUES.

Отмеченная форма создаст вот такие вещи в sqlc запросе

sql
-- name: CreateTableMeta :one  
INSERT INTO dc.table_meta (  
    name,  
    description,  
    schema_id,  
    table_type_id,  
    domain_id,  
    is_deleted,  
    created_at,  
    updated_at,  
    is_get_dict,  
    user_id  
) VALUES (  
    @name,  
    @description,  
    @schema_id,  
    @table_type_id,  
    @domain_id,  
    false,  
    now(),  
    now(),  
    false,  
    @user_id  
)  
RETURNING *;

proto-файл:

proto
service TableMetaService {  
  rpc CreateTableMeta(CreateTableMetaRequest) returns (CreateTableMetaResponse);  
}  

// Строка таблицы dc.table_meta: все колонки, как их вернёт SELECT *.  
message TableMeta {  
  int64 id = 1;  
  string name = 2;  
  string description = 3;  
  int64 schema_id = 4;  
  int64 table_type_id = 5;  
  int64 domain_id = 6;  
  bool is_deleted = 7;  
  google.protobuf.Timestamp created_at = 8;  
  google.protobuf.Timestamp updated_at = 9;  
  bool is_get_dict = 10;  
  int64 user_id = 11;  
}  

// Вставка CreateTableMeta: параметры вызова.  
message CreateTableMetaRequest {  
  string name = 1;  
  string description = 2;  
  int64 schema_id = 3;  
  int64 table_type_id = 4;  
  int64 domain_id = 5;  
  int64 user_id = 6;  
}  

// Вставка CreateTableMeta: одна строка.  
message CreateTableMetaResponse {  
  TableMeta table_meta = 1;  
}

READ

Черновик одной выборки. Живёт до «Добавить», сбрасывается при смене таблицы. По умолчанию отмечены все колонки в «показать», фильтров нет.

Верхние поля

Поле Что делает
Название Имя метода sqlc, обязательное, уникальное по всему schema.json. Кнопка‑подсказка складывает имя из отмеченных WHERE.
Query Annotation one или many. Смена перерисовывает форму — от many зависит состав.
Pagination Чекбокс, показывается только при many. Даёт парный запрос Count<имя> :one (total_items, total_pages) с теми же условиями. Если такое имя занято — файл не перезаписывается вовсе.

Join (только если у таблицы описаны цепочки)

Список чекбоксов по цепочкам из раздела JOINS, рядом — расшифровка звеньев (INNER dc.table t · ...). Включение цепочки перерисовывает форму: под основной сеткой появляется отдельная сетка колонок на каждое звено.

Сетка «Поля»

Строка на каждую колонку таблицы (имя + SQL‑тип), колонки:

  • показать — колонка попадает в SELECT.
  • добавить в WHERE — обязательный фильтр: колонка = значение.
  • WHERE OPTIONAL — необязательный: (значение IS NULL OR колонка = значение) с приведением типа из DDL → optional‑параметр sqlc.narg.
  • EXACT WHERE — текстовое поле. Самостоятельное условие через AND, даже если ни WHERE, ни WHERE OPTIONAL не отмечены. Если отмечены — переопределяет значение внутри их условия. Пустое пишется как null.
  • ORDER BY / ORDER BY OPTIONAL — только при many. Обычные колонки сортируются всегда; optional — выбираются параметром order_by (имя строкой) через CASE, направление общее (order ASC/DESC). У many порядок есть всегда, иначе постраничная выборка повторяет и теряет строки.

Сетки приджойненных таблиц

Одна на каждое звено, под заголовком alias · dc.table / join <цепочка>. Те же колонки сетки. Колонка выходит под именем с алиасом (alias_columno.id AS o_id), это же имя носит параметр и поле в .proto. Выборка с join“ом обязана отметить хоть одну колонку — иначе SELECT * смешал бы столбцы, и запрос пропускается. Колонки стороны, которую соединение вправе оставить пустой (LEFT, FULL), объявляются optional.

Нижние поля

  • Custom WHERE — своё условие, приклеивается через AND (сужает, не заменяет).
  • Custom Query — если заполнено, отменяет всё остальное: колонки, фильтры, custom_where игнорируются, из настроек берутся только имя и аннотация; LIMIT/RETURNING пишет автор.

Показать 1 запись

sql
-- name: GetTableMetaById :one  
SELECT  
    table_meta.id,  
    table_meta.name,  
    table_meta.description,  
    table_meta.schema_id,  
    table_meta.table_type_id,  
    table_meta.domain_id,  
    table_meta.is_deleted,  
    table_meta.created_at,  
    table_meta.updated_at,  
    table_meta.is_get_dict,  
    table_meta.user_id  
FROM dc.table_meta  
WHERE table_meta.id = @id  
LIMIT 1;
proto
rpc GetTableMetaById(GetTableMetaByIdRequest) returns (GetTableMetaByIdResponse);

// Выборка GetTableMetaById: параметры вызова.  
message GetTableMetaByIdRequest {  
  int64 id = 1;  
}

// Выборка GetTableMetaById: одна строка.  
message GetTableMetaByIdResponse {  
  TableMeta table_meta = 1;  
}

// Строка таблицы dc.table_meta: все колонки, как их вернёт SELECT *.  
message TableMeta {  
  int64 id = 1;  
  string name = 2;  
  string description = 3;  
  int64 schema_id = 4;  
  int64 table_type_id = 5;  
  int64 domain_id = 6;  
  bool is_deleted = 7;  
  google.protobuf.Timestamp created_at = 8;  
  google.protobuf.Timestamp updated_at = 9;  
  bool is_get_dict = 10;  
  int64 user_id = 11;  
}

Поиск по названию

CUSTOM WHERE = lower(name) LIKE '%' || lower(sqlc.arg(search_name)) || '%'

sql
-- name: GetTableMetasSearchName :many  
SELECT  
    table_meta.id,  
    table_meta.name,  
    table_meta.description,  
    table_meta.schema_id,  
    table_meta.table_type_id,  
    table_meta.domain_id,  
    table_meta.is_deleted,  
    table_meta.created_at,  
    table_meta.updated_at,  
    table_meta.is_get_dict,  
    table_meta.user_id  
FROM dc.table_meta  
WHERE table_meta.is_deleted = false  
  AND (lower(name) LIKE '%' || lower(sqlc.arg(search_name)) || '%')  
ORDER BY CASE WHEN sqlc.arg('order')::text <> 'DESC' THEN table_meta.id END ASC,  
    CASE WHEN sqlc.arg('order')::text = 'DESC' THEN table_meta.id END DESC;
proto
rpc GetTableMetasSearchName(GetTableMetasSearchNameRequest) returns (GetTableMetasSearchNameResponse);

// Выборка GetTableMetasSearchName: параметры вызова. Порядок по умолчанию — id.  
message GetTableMetasSearchNameRequest {  
  string search_name = 1;  
  // допустимые значения: ASC, DESC  
  string order = 2;  
}

// Выборка GetTableMetasSearchName: все найденные строки.  
message GetTableMetasSearchNameResponse {  
  repeated TableMeta rows = 1;  
}

Добавить Paging

sql
-- name: GetTableMetasWithPagination :many  
SELECT  
    table_meta.id,  
    table_meta.name,  
    table_meta.description,  
    table_meta.schema_id,  
    table_meta.table_type_id,  
    table_meta.domain_id,  
    table_meta.is_deleted,  
    table_meta.created_at,  
    table_meta.updated_at,  
    table_meta.is_get_dict,  
    table_meta.user_id  
FROM dc.table_meta  
WHERE table_meta.is_deleted = false  
ORDER BY CASE WHEN sqlc.arg('order')::text <> 'DESC' THEN table_meta.id END ASC,  
    CASE WHEN sqlc.arg('order')::text = 'DESC' THEN table_meta.id END DESC  
LIMIT @page_limit::int OFFSET (sqlc.arg('page')::int-1)*sqlc.arg('page_limit')::int;  

-- name: CountGetTableMetasWithPagination :one  
SELECT  
    count(*)                                                        AS total_items,  
    ceil(count(*)::numeric / GREATEST(@page_limit::int, 1))::bigint AS total_pages  
FROM dc.table_meta  
WHERE table_meta.is_deleted = false;
proto
rpc GetTableMetasWithPagination(GetTableMetasWithPaginationRequest) returns (GetTableMetasWithPaginationResponse);

// Выборка GetTableMetasWithPagination: параметры вызова. Порядок по умолчанию — id.  
message GetTableMetasWithPaginationRequest {  
  // допустимые значения: ASC, DESC  
  string order = 1;  
  int32 page_limit = 2;  
  int32 page = 3;  
}

// Выборка GetTableMetasWithPagination: страница данных и пагинация.  
message GetTableMetasWithPaginationResponse {  
  repeated TableMeta data = 1;  
  Pagination pagination = 2;  
}

Update

Черновик одного UPDATE. По умолчанию все колонки отмечены в «изменения», фильтров нет.

Верхние поля

Поле Что делает
Название Имя метода sqlc, обязательное, уникальное по всему файлу. Подсказка — по отмеченным WHERE.
Query Annotation exec или one. one добавляет RETURNING *. Перерисовки формы не вызывает.

Сетка «Поля»

Строка на каждую колонку таблицы (имя + SQL‑тип):

  • изменения — колонка попадает в SET.
  • значение изменения — текст. Пусто = «приходит параметром» → @имя_колонки (как в CREATE, не null). Заполнено — подставляется как есть (now(), подзапрос).
  • WHERE — обязательный фильтр: колонка = значение.
  • Optional WHERE — необязательный: (значение IS NULL OR колонка = значение) с приведением типа, optional‑параметр.
  • значение WHERE — переопределяет значение в условии; пусто = null.

Нижние поля

  • Custom WHERE — приклеивается через AND.
  • Custom Query — заполнено → отменяет всё: из настроек только имя и аннотация.

Проблемы генерации

  • нет ни одной изменяемой колонки → запрос пропускается (fatal).
  • нет WHEREfatal=False, пишется с пометкой «изменит всю таблицу».
  • первичный ключ в SET получает значение параметромfatal=False, предупреждение (ключ в WHERE — норма, UpdateById только так и пишется).

sql
-- name: UpdateTableMetaById :exec  
UPDATE dc.table_meta  
SET name = @name,  
    description = @description,  
    schema_id = @schema_id,  
    table_type_id = @table_type_id,  
    domain_id = @domain_id,  
    updated_at = now(),  
    is_get_dict = @is_get_dict  
WHERE table_meta.id = @id;
proto
rpc UpdateTableMetaById(UpdateTableMetaByIdRequest) returns (UpdateTableMetaByIdResponse);

// Изменение UpdateTableMetaById: параметры вызова.  
message UpdateTableMetaByIdRequest {  
  string name = 1;  
  string description = 2;  
  int64 schema_id = 3;  
  int64 table_type_id = 4;  
  int64 domain_id = 5;  
  bool is_get_dict = 6;  
  int64 id = 7;  
}  

// Изменение UpdateTableMetaById: ответ пустой — запрос ничего не возвращает.  
message UpdateTableMetaByIdResponse {}

Delete

Черновик одного удаления.

Верхние поля

Поле Что делает
Название Как везде — обязательное, уникальное по файлу.
Режим Выпадающий список: DELETE / SOFT DELETE / UNDELETE (app.py:106). Смена перерисовывает форму — от режима зависит набор колонок сетки. Аннотации у Delete нет: режим сам решает тип запроса.

Режимы:

  • DELETE — физическое DELETE FROM, ничего не проставляет.
  • SOFT DELETE и UNDELETE — одинаковый UPDATE ... SET. Различаются только тем, что автор пишет в значения (мягкое ставит флаг/дату, обратное возвращает прежнее). SET_MODES = (SOFT DELETE, UNDELETE).

Сетка «Поля»

При SOFT DELETE/UNDELETE — 6 колонок (update-grid): изменения, значение изменения, WHERE, Optional WHERE, значение WHERE.
При DELETE — 3 колонки (delete-grid): только WHERE, Optional WHERE, значение WHERE (ключей set/set_value у физического удаления в schema.json нет вовсе).

По умолчанию в «изменения» не отмечена ни одна колонка — проставляют обычно две‑три служебные, а не всю таблицу.

Нижние поля

  • Custom WHERE — через AND.
  • Custom Query — отменяет всё остальное.

Проблемы генерации

  • нет WHERE (любой режим) → fatal=False, «заденет всю таблицу», но пишется.
  • SOFT DELETE/UNDELETE без единой проставляемой колонки → пропуск (fatal).
  • первичный ключ в SET получает значение параметромfatal=False, предупреждение.

Список ниже формы

У всех трёх форм — карточка «Добавленные запросы · N» с зафиксированными значениями (у Update/Delete строки: изменения, WHERE, Optional WHERE, Custom WHERE, Custom Query; у Delete строка «изменения» показывается только для soft-режимов). Кнопки Редактировать / Скопировать / Удалить; копия Delete несёт режим и его колонки.

Реальное удаление

sql
-- name: DeleteTableMetaById :exec  
DELETE FROM dc.table_meta  
WHERE table_meta.id = @id;
proto
rpc DeleteTableMetaById(DeleteTableMetaByIdRequest) returns (DeleteTableMetaByIdResponse);

// Удаление DeleteTableMetaById: параметры вызова.  
message DeleteTableMetaByIdRequest {  
  int64 id = 1;  
}

// Удаление DeleteTableMetaById: ответ пустой — запрос ничего не возвращает.  
message DeleteTableMetaByIdResponse {}

Мягкое удаление

sql
-- name: SoftDeleteTableMeta :exec  
UPDATE dc.table_meta  
SET is_deleted = true  
WHERE table_meta.id = @id;
proto
// Мягкое удаление SoftDeleteTableMeta: параметры вызова.  
message SoftDeleteTableMetaRequest {  
  int64 id = 1;  
}  

// Мягкое удаление SoftDeleteTableMeta: ответ пустой — запрос ничего не возвращает.  
message SoftDeleteTableMetaResponse {}

Мягкое восстановление после удаления

sql
-- name: UndeleteTableMeta :exec  
UPDATE dc.table_meta  
SET is_deleted = false  
WHERE table_meta.id = @id;
proto
rpc UndeleteTableMeta(UndeleteTableMetaRequest) returns (UndeleteTableMetaResponse);

// Обратное удаление UndeleteTableMeta: параметры вызова.  
message UndeleteTableMetaRequest {  
  int64 id = 1;  
}  

// Обратное удаление UndeleteTableMeta: ответ пустой — запрос ничего не возвращает.  
message UndeleteTableMetaResponse {}

Join

Черновик одной цепочки. Цепочка описывает связь таблицы, а не запрос: одну и ту же цепочку включают в себя несколько выборок Read, поэтому она лежит в разделе JOINS рядом с CRUD, а не внутри него.

Название

Имя цепочки. Обязательное, уникально в пределах таблицы — в sqlc оно не попадает, кнопки‑подсказки нет. Под полем — подсказка JOIN_HELP: «Цепочка — одна или несколько таблиц, присоединяемых по порядку. Связь „многие ко многим“ — два звена: связующая таблица, потом целевая».

Звенья

Форма начинается с одного звена. Кнопка «Добавить таблицу»добавляет ещё, у звена (если их больше одного) есть кнопка «Убрать». Добавление/удаление звена перерисовывает форму. На каждое звено:

Контрол Что делает
тип INNER / LEFT / RIGHT / FULL (query_gen.py:121). Разворачивается в <тип> JOIN. От типа зависит optional в .proto: LEFT/FULL могут не дать строк приджойненной стороне, RIGHT/FULL — своей.
таблица Выпадающий список всех таблиц схемы. Выбор подставляет алиас, если он ещё не написан руками (sgbuddy/app.py:2709 → sgbuddy/app.py:2675): dc."order"order, занятый → order2. Перерисовывает форму.
алиас Простое имя латиницей, не слово SQL (sgbuddy/query_gen.py:332). Из него собираются имя колонки на выходе (t_name) и имя её параметра (@t_name), поэтому @"my alias" не бывает. Не должен совпадать с алиасом соседнего звена и с именем своей таблицы.
ON Условие соединения, пишется руками и уходит в SQL ровно как написано. Под полем — напоминание: своя таблица в условии — dc.<short_name>, приджойненная — под своим алиасом.

Проверки при «Добавить»

По каждому звену по порядку: таблица выбрана, алиас годится (is_alias) и не занят, ON не пуст. Первая же ошибка показывается текстом звено N: ... и запись не сохраняется.

Ссылки из Read

  • Переименование цепочки ведёт за собой joins и joined_columns во всех выборках Read этой таблицы — иначе выборка со ссылкой на исчезнувшее имя не соберётся.
  • Удаление вычищает цепочку и её колонки из выборок Read.

Список ниже формы

Карточка «Добавленные join“ы · N». По каждой цепочке — звенья в порядке соединения: INNER JOIN dc.table t / ON t.id = .... Кнопки Редактировать / Скопировать / Удалить; копия несёт все звенья (цепочки соседних таблиц часто отличаются одним звеном — копию быстрее поправить).

sql
-- name: GetTableMetaByIdWithTypeName :one  
SELECT  
    table_meta.id,  
    table_meta.name,  
    table_meta.description,  
    table_meta.schema_id,  
    table_meta.table_type_id,  
    table_meta.domain_id,  
    table_meta.is_deleted,  
    table_meta.created_at,  
    table_meta.updated_at,  
    table_meta.is_get_dict,  
    table_meta.user_id,  
    table_type.name AS table_type_name  
FROM dc.table_meta  
INNER JOIN dc.table_type table_type ON table_meta.table_type_id = table_type.id  
WHERE table_meta.id = @id  
LIMIT 1;
proto
rpc GetTableMetaByIdWithTypeName(GetTableMetaByIdWithTypeNameRequest) returns (GetTableMetaByIdWithTypeNameResponse);

// Выборка GetTableMetaByIdWithTypeName: параметры вызова.  
message GetTableMetaByIdWithTypeNameRequest {  
  int64 id = 1;  
}  

// Выборка GetTableMetaByIdWithTypeName: строка ответа — только отмеченные колонки.  
message GetTableMetaByIdWithTypeNameRow {  
  int64 id = 1;  
  string name = 2;  
  string description = 3;  
  int64 schema_id = 4;  
  int64 table_type_id = 5;  
  int64 domain_id = 6;  
  bool is_deleted = 7;  
  google.protobuf.Timestamp created_at = 8;  
  google.protobuf.Timestamp updated_at = 9;  
  bool is_get_dict = 10;  
  int64 user_id = 11;  
  string table_type_name = 12;  
}

Сгенерированные файлы примеров