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 для нужных таблиц.
Для примера нам хватит всего двух таблиц.
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 запросе
-- 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-файл:
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, направление общее (orderASC/DESC). Уmanyпорядок есть всегда, иначе постраничная выборка повторяет и теряет строки.
Сетки приджойненных таблиц
Одна на каждое звено, под заголовком alias · dc.table / join <цепочка>. Те же колонки сетки. Колонка выходит под именем с алиасом (alias_column → o.id AS o_id), это же имя носит параметр и поле в .proto. Выборка с join“ом обязана отметить хоть одну колонку — иначе SELECT * смешал бы столбцы, и запрос пропускается. Колонки стороны, которую соединение вправе оставить пустой (LEFT, FULL), объявляются optional.
Нижние поля
- Custom WHERE — своё условие, приклеивается через
AND(сужает, не заменяет). - Custom Query — если заполнено, отменяет всё остальное: колонки, фильтры,
custom_whereигнорируются, из настроек берутся только имя и аннотация;LIMIT/RETURNINGпишет автор.
Показать 1 запись

-- 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;
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)) || '%'

-- 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;
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

-- 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;
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). - нет WHERE →
fatal=False, пишется с пометкой «изменит всю таблицу». - первичный ключ в SET получает значение параметром →
fatal=False, предупреждение (ключ вWHERE— норма,UpdateByIdтолько так и пишется).

-- 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;
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 несёт режим и его колонки.
Реальное удаление

-- name: DeleteTableMetaById :exec
DELETE FROM dc.table_meta
WHERE table_meta.id = @id;
rpc DeleteTableMetaById(DeleteTableMetaByIdRequest) returns (DeleteTableMetaByIdResponse);
// Удаление DeleteTableMetaById: параметры вызова.
message DeleteTableMetaByIdRequest {
int64 id = 1;
}
// Удаление DeleteTableMetaById: ответ пустой — запрос ничего не возвращает.
message DeleteTableMetaByIdResponse {}
Мягкое удаление

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

-- name: UndeleteTableMeta :exec
UPDATE dc.table_meta
SET is_deleted = false
WHERE table_meta.id = @id;
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 = .... Кнопки Редактировать / Скопировать / Удалить; копия несёт все звенья (цепочки соседних таблиц часто отличаются одним звеном — копию быстрее поправить).


-- 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;
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;
}