Компания «Карельский разработчик» представляет обновление 0.4.0 «Сегежа» для системы управления содержимым «ГИРВАС». Версия сменяет кодовую стадию «Шуя» и получает наименование в честь реки Сегежа — водного объекта Республики Карелия, продолжающего традицию присвоения версиям CMS имён карельских рек и озёр. Эта традиция подчёркивает региональную принадлежность продукта и его включённость в ландшафт отечественных технологических разработок.
Релиз 0.4.0 является самым масштабным за всю историю проекта с 2021 года. Он объединяет несколько стратегических направлений: приведение системы в соответствие с требованиями Федерального закона №152-ФЗ «О персональных данных», закладку фундамента для OAuth-провайдинга, внедрение слоя SQL-диалектов с поддержкой MySQL и PostgreSQL, а также ряд улучшений административной панели и публичной части сайта.
152-ФЗ: полное техническое покрытие на уровне ядра
Одним из ключевых направлений релиза стало приведение CMS «ГИРВАС» в соответствие с требованиями Федерального закона №152-ФЗ «О персональных данных». В версии 0.4.0 реализовано полное техническое покрытие ключевых требований закона на уровне ядра системы — без необходимости устанавливать сторонние плагины или разрабатывать персональные решения.
Законодательство о персональных данных предъявляет к операторам целый ряд требований: необходимость фиксировать факт получения согласия субъекта, обеспечивать доказательства этого согласия, хранить данные на территории Российской Федерации, предоставлять субъекту доступ к его данным и возможность отзыва согласия. Для сайтов, которые собирают данные посетителей через формы обратной связи, регистрацию или cookie-баннеры, соблюдение этих требований является обязательным.






В версии 0.4.0 реализованы следующие механизмы:
- Версионирование юридических документов. Каждая публикация юридического документа — политики конфиденциальности, пользовательского соглашения, согласия на обработку данных — создаёт неизменяемый снимок с номером версии, датой вступления в силу и автором. История версий неизменяема, что соответствует требованиям аудита. Посетитель сайта может просмотреть конкретную версию документа, с которой он соглашался.
- Фиксация согласий. Система фиксирует согласие пользователя с конкретным документом и версией его редакции, с учётом локали, в которой пользователь видел документ, а также IP-адреса и User-Agent. Согласия фиксируются при отправке динамических форм, при регистрации на сайте и через cookie-баннер — включая согласия анонимных посетителей.
- Отзыв согласия. Пользователь может отозвать согласие через свой профиль. Администратор системы также может отозвать согласие с указанием причины. Оба действия фиксируются в журнале с различием между отзывом субъектом и отзывом администратором.
- Экспорт данных субъекта. Система формирует архив с данными пользователя по запросу: профиль, согласия, отчёты и манифест с метаданными о том, кто выгрузил данные и когда. Это обеспечивает выполнение требований статьи 14 закона.
- Защита от подделки IP-адреса. Реализован механизм определения реального IP-адреса посетителя с учётом доверенных прокси. Если сервер не входит в список доверенных, заголовки X-Forwarded-For игнорируются — это предотвращает подделку IP при логировании.
- Анонимизация персональных данных. Вместо удаления данных система выполняет их обезличивание: логин, email, пароль и метаданные заменяются на анонимные значения. Это соответствует требованиям статьи 5 закона и сохраняет структуру данных для аудита.
- Ротация логов. Отчёты старше заданного срока архивируются в отдельную таблицу и удаляются из основной. Настройка выполняется через административную панель, ротация может запускаться автоматически по расписанию.
- Cookie-баннер. Встроенный баннер для всех посетителей сайта, включая анонимных, с фиксацией согласия и защитой от дубликатов. Согласие анонимного посетителя фиксируется с идентификатором 0, что позволяет отличать его от согласий зарегистрированных пользователей.
- Логирование всех событий. CMS «ГИРВАС» в рамках реализации полного технического покрытия на уровне ядра 152-ФЗ была дополнена новым разделом «Отчеты CMS», где отображаются все события в системе, включая отслеживание просмотра и взаимодействия с персональными данными.
Для владельцев сайтов это означает, что при использовании «ГИРВАС» им не придётся искать внешние решения для compliance-задач — система обеспечивает техническую сторону вопроса, оставляя за оператором организационные меры: назначение ответственного за обработку данных, разработку внутренних документов и обучение сотрудников.
Стоит отметить, что ни одна из популярных CMS на рынке не реализует полный цикл привязки согласия к версии юридического документа на уровне ядра. Отдельные плагины закрывают часть задачи, но это всегда сторонние расширения, не покрывающие весь спектр требований 152-ФЗ. «ГИРВАС» — одна из немногих систем, где это реализовано на уровне ядра.
Новый раздел «Отчеты CMS»
Это первое внедрение за последний год нового раздела, который фактически стал фундаментом для технического покрытия требований 152-ФЗ. Раздел был поплнен тремя подразделами:
- Общая сводка. Отчет отображает общую информацию по всем подразделам отчетности системы: от топа активных пользователей до последних событий.
- Контент. Статистика по контенту в системе и последние события, связанные с ним.
- Безопасность. Статистика по взаимодействию с системой с точки зрения безопасности: успешные/неуспешные входы в административную панель или сайт, блокировки/разблокировки, просмотры персональных данных и отчетов, перечень IP-адресов (с которых был успешный вход в административную панель) и т.д.



Новый интерактивные элементы и возможности «NadvoTE»
CMS «ГИРВАС» в версии 0.4.0 был пополнен рядом новых интерактивных элементов:
Галереи
Галереи в CMS «ГИРВАС» позволяют встраивать интерактивную карусель изображений — в дальнейшем они начнут поддерживать разные режимы отображения изображений. Парсер MarkDown-разметки «NadvoParse» также позволяет внедрять галереи при помощи следующего синтаксиса:
[gallery]





[/gallery]
Пример отображения галереи

Искатель данных
Интерактивный элемент для поиска данных успешно интегрирован в подразделах административной панели CMS «ГИРВАС» и позволяет находить нужные элементы сущностей системы на страницах с их списками (записи, статические страницы, категории, группы пользователей и т.д.).
Вкладки
Вкладки позволяют систематизировать данные и отсортировать их в пределах общего контейнера для экономии пространства.

Новые инструменты интерактивного редактора «NadvoTE»

Интерактивный редактор «NadvoTE», разработанный специально для CMS «ГИРВАС» компанией «Карельский разработчик» была пополнена новыми инструментами:
- Отмена — отменить последнее изменение.
- Вернуть — вернуть отмененное изменение.
- Галерея — встроить интерактивную галерею изображений.
- Эмодзи — вставка эмодзи из первичного набора.
Кроме того, теперь редактор поддерживает горячие клавиши для работы с инструментарием в быстром режиме:
Ctrl + Z— отменить последнее изменение.Ctrl + Y— повторить последнее изменение.Ctrl + B— сделать выделение жирным (будет обрамлено**текст**).Ctrl + I— сделать выделение курсивным (будет обрамлено*текст*).Ctrl + U— сделать выделение подчеркнутым (будет обрамлено~~текст~~).
Редактор «NadvoTE» теперь также позволяет обрамлять выделение различными кавычками и скобками — достаточно выделить текст и нажать на клавиатуре клавишу или их сочетание для печати кавычки или скобки. «NadvoTE» становится еще более простым в использовании.
OAuth-провайдер: фундамент для единого входа
В версии 0.4.0 заложен фундамент для встроенного OAuth-провайдера — механизма, который позволит любому сайту на CMS «ГИРВАС» выступать в роли сервера авторизации для сторонних приложений.
OAuth — это открытый стандарт авторизации, который позволяет пользователям предоставлять сторонним сервисам доступ к своим данным без передачи пароля. Например, пользователь может авторизоваться на внешнем сервисе через сайт компании, не создавая для этого отдельной учётной записи. Это открывает путь к единому входу между сайтами на «ГИРВАС» и подключению внешних сервисов без необходимости разрабатывать собственные решения.
Заложенная архитектура включает поддержку протокола Authorization Code Grant с обязательной поддержкой PKCE (S256) — механизма, повышающего безопасность при работе с публичными клиентами. Реализованы строгая валидация redirect_uri, одноразовые коды авторизации с коротким сроком жизни, ротация refresh-токенов при каждом обновлении и хеширование client_secret.
Наличие встроенного OAuth-провайдера в бесплатной CMS — редкость для российского рынка. Обычно такие возможности реализуются через платные модули или кастомную разработку. «ГИРВАС» закладывает эту функциональность на уровне ядра, что открывает путь к единому входу между сайтами на этой CMS.
Серверная часть провайдера реализована и протестирована. Интерфейсы для администратора и пользователя — consent-форма, раздел управления выданными доступами, интерфейс верификации клиентов — появятся в следующих обновлениях.
SQL-диалекты: поддержка MySQL и PostgreSQL
Одним из наиболее значительных технических изменений версии стало внедрение слоя SQL-диалектов — механизма абстракции над системами управления базами данных.
Ранее CMS «ГИРВАС» работала исключительно с PostgreSQL и MySQL. Теперь обе СУБД работают через единый слой абстракции — это также открывает путь для внедрения остальных СУБД. Это означает, что владелец сайта может выбрать ту СУБД, которая ему удобнее, а система будет работать одинаково стабильно в обоих случаях.
Архитектура диалектов построена на логических типах данных: вместо физических типов конкретной СУБД система оперирует абстрактными понятиями — идентификатор, строка, текст, JSON, булево значение, временная метка. Каждый диалект самостоятельно преобразует логический тип в физический для своей СУБД. Это позволяет добавлять поддержку новых СУБД без правки ядра — достаточно добавить один класс диалекта. Расширение перечня поддерживаемых СУБД будет расти с последующими обновлениями, среди которых станут: MariaDB, SQLite и другие — теперь это будет сделать проще с SQL-диалектами.
Для разработчиков это означает, что расширять систему и добавлять новые модули стало проще: код не привязан к конкретной СУБД, а работает через единый интерфейс. Для владельцев сайтов — что «ГИРВАС» можно развернуть на том хостинге, который уже используется, без необходимости миграции на PostgreSQL или MySQL.
Мультиязычность настроек сайта
В версии 0.4.0 реализована мультиязычность настроек сайта. Ранее мультиязычность ограничивалась контентом — записями, страницами, категориями. Теперь настройки сайта также могут иметь разные значения для разных языков.
Мультиязычными стали: название сайта, описание сайта и ключевые слова. Формат хранения — JSON-объект, где ключом является код локали, а значением — текст на соответствующем языке. При первом сохранении старые значения автоматически мигрируют в новый формат, что обеспечивает обратную совместимость.
Для сайтов, работающих на несколько языков, это означает, что заголовок и описание сайта могут отображаться на языке посетителя, а не быть едиными для всех. Это улучшает как пользовательский опыт, так и поисковую оптимизацию для мультиязычных сайтов.
Административная панель: поиск и сортировка
В версии 0.4.0 в административной панели реализован поиск и сортировка во всех разделах со списками контента: пользователи, группы, согласия, записи, статические страницы, категории, комментарии, выборки, формы и контент-блоки.
Поиск работает по GET-параметру в том же разделе, без перехода на отдельные страницы. Сортировка доступна по шести правилам: по дате создания и обновления в обоих направлениях, а также по алфавиту в обоих направлениях. Параметры поиска и сортировки сохраняются при переходе по страницам пагинации — это значит, что пользователь не теряет контекст при просмотре больших списков.
Для администраторов сайтов это означает, что работа с большими объёмами контента стала заметно удобнее. Раньше, чтобы найти нужную запись среди сотен, приходилось листать страницы. Теперь достаточно ввести часть названия или выбрать правило сортировки.
Интеграция с Яндекс.Метрикой
В версии 0.4.0 реализована встроенная интеграция с Яндекс.Метрикой. Администратор указывает числовой идентификатор счётчика в настройках поисковой оптимизации, после чего система автоматически выводит стандартный скрипт Метрики на всех страницах публичной части сайта.
Реализована двойная валидация: на этапе сохранения настройки из введённого значения удаляются все нецифровые символы, а на этапе вывода выполняется проверка, что значение является числом. Это защищает от ошибок ввода и от попыток внедрить произвольный код через поле настройки.
Интеграция уже работает в теме «default», а параметр для внедрения Метрики доступен в подразделе «Поисковая оптимизация» раздела «Настройки CMS».

Технические усовершенствования ядра
Помимо перечисленного, версия 0.4.0 содержит значительные доработки компонента QueryBuilder — внутреннего построителя SQL-запросов.
Добавлена поддержка JOIN-ов (INNER, LEFT, RIGHT, FULL) с адаптивной генерацией условий для MySQL и PostgreSQL. Реализован класс для построения CASE-выражений, включая специализированные методы для JSON-поиска. Добавлена полноценная поддержка индексов: создание и удаление с учётом типов BTREE, HASH, GIST, GIN, SPGIST, BRIN, FULLTEXT, а также UNIQUE, CONCURRENTLY, IF NOT EXISTS и частичных индексов с WHERE-условиями.
При установке CMS теперь автоматически создаются оптимизирующие индексы для всех таблиц, включая GIN-индексы для JSONB-полей в PostgreSQL и частичный индекс для опубликованных записей. Ошибки создания индексов логируются без прерывания процесса установки.
Для владельцев сайтов это означает, что система работает быстрее и стабильнее даже при росте объёма контента. Для разработчиков — что расширять систему и добавлять новые модули стало проще за счёт более гибкого построителя запросов.
Исправления
В версии 0.4.0 устранён ряд ошибок:
- Cookie-баннер показывался повторно при наличии cookie allowCookies — исправлена логика проверки cookie.
- Batch-вставка на MySQL могла работать некорректно при определённых настройках СУБД — заменена на транзакцию с поштучным добавлением записей.
- Утечка чувствительных полей в debug-выводе — добавлена маскировка client_secret, code_verifier, access_token, refresh_token и паролей.
- Регенерация секрета OAuth-клиента не сохраняла новый секрет — исправлено.
- Пустой scope при обновлении токена через refresh — исправлено.
- Периодическая невозможность работы в мультиязычном режиме в административной панели.
- Невозможность отправки запросов через фронт в бекенд при одновременной работы с нескольких закладок браузера.
Миграция
Обновление файловой системы
Мы настоятельно рекомендуем получать обновление через официальный репозиторий CMS «ГИРВАС». Если Вы его используете по умолчанию, то в корне Вашего проекта выполните консольную команду git pull, предварительно переключившись на ветку production (если ранее использовали другую ветку).
После обновления файловой системы обязательно очистите кэш системы: rm -R ./cache/* (путь может отличаться в зависимости от фактического Вашего расположения в системе).
Миграция базы данных
После успешного обновления файлов CMS необходимо провести миграцию базы данных. Для этого потребуется выполнить несколько SQL-запросов к Вашей базе данных, в зависимости от вида Вашего СУБД.
Миграция MySQL
В отличие от PostgreSQL, миграция MySQL не транзакционна — каждый ALTER TABLE
фиксируется сразу. Если миграция упадёт на середине — часть изменений останется.
Поэтому процесс разбит на три этапа:
- Предварительная проверка — убеждаемся, что данные влезают в новые типы.
- Миграция — применяем изменения.
- Завершающая проверка — убеждаемся, что всё на месте.
Перед началом обязательно сделайте бэкап:
mysqldump -h HOST -u USER -p DBNAME > backup_before_migration.sql
Ограничения MySQL-миграции
Из-за различий между СУБД некоторые объекты PostgreSQL не имеют аналогов в MySQL:
- GIN-индексы для JSONB-полей (
idx_entries_texts_gin,idx_entries_metadata_gin,idx_pages_static_texts_gin,idx_pages_static_versions_texts_gin) — в MySQL не создаются. Поиск по JSON будет работать, но медленнее. - Частичные индексы (
idx_entries_published,idx_users_consents_active,idx_users_consents_revoked_by) — в MySQL создаются как обычные, безWHERE. Функциональность сохраняется, но эффективность фильтрации чуть ниже.
Это ограничения СУБД, а не CMS. Функциональность системы сохраняется полностью.
Предварительная проверка
-- =====================================================================
-- PRE-CHECK перед миграцией CMS «ГИРВАС» 0.4.0 «Сегежа»
-- СУБД: MySQL 8.0+
-- Запускать ПЕРЕД миграцией
--
-- Если хотя бы одна проверка вернёт ПРОБЛЕМНЫЕ строки — СТОП,
-- миграцию запускать нельзя.
-- =====================================================================
SET NAMES utf8mb4;
-- ---------------------------------------------------------------------
-- ПРОВЕРКА 1. MAX(id) — не превышает ли 2^31-1 (2147483647)
-- Если где-то больше — приведение id к int обрежет данные.
-- ---------------------------------------------------------------------
SELECT "configurations" AS table_name, MAX(id) AS max_id FROM configurations
UNION ALL SELECT "content_blocks", MAX(id) FROM content_blocks
UNION ALL SELECT "entries", MAX(id) FROM entries
UNION ALL SELECT "entries_categories", MAX(id) FROM entries_categories
UNION ALL SELECT "entries_comments", MAX(id) FROM entries_comments
UNION ALL SELECT "entries_samples", MAX(id) FROM entries_samples
UNION ALL SELECT "forms", MAX(id) FROM forms
UNION ALL SELECT "forms_data", MAX(id) FROM forms_data
UNION ALL SELECT "metrics", MAX(id) FROM metrics
UNION ALL SELECT "pages_static", MAX(id) FROM pages_static
UNION ALL SELECT "reports", MAX(id) FROM reports
UNION ALL SELECT "users", MAX(id) FROM users
UNION ALL SELECT "users_groups", MAX(id) FROM users_groups
UNION ALL SELECT "users_registration_submits", MAX(id) FROM users_registration_submits
UNION ALL SELECT "users_sessions", MAX(id) FROM users_sessions
UNION ALL SELECT "web_channels", MAX(id) FROM web_channels;
-- ---------------------------------------------------------------------
-- ПРОВЕРКА 2. Нет ли данных длиннее лимитов при text → varchar(N)
-- Все значения должны быть 0.
-- ---------------------------------------------------------------------
SELECT "configurations.name" AS column_path, COUNT(*) AS too_long
FROM configurations WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "entries.name", COUNT(*) FROM entries WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "entries_categories.name", COUNT(*) FROM entries_categories WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "entries_samples.name", COUNT(*) FROM entries_samples WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "forms.name", COUNT(*) FROM forms WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "pages_static.name", COUNT(*) FROM pages_static WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "users.login", COUNT(*) FROM users WHERE CHAR_LENGTH(login) > 255
UNION ALL
SELECT "users.email", COUNT(*) FROM users WHERE CHAR_LENGTH(email) > 255
UNION ALL
SELECT "users_groups.name", COUNT(*) FROM users_groups WHERE CHAR_LENGTH(name) > 255
UNION ALL
SELECT "users_registration_submits.submitToken",
COUNT(*) FROM users_registration_submits WHERE CHAR_LENGTH(submitToken) > 255
UNION ALL
SELECT "users_registration_submits.refusalToken",
COUNT(*) FROM users_registration_submits WHERE CHAR_LENGTH(refusalToken) > 255
UNION ALL
SELECT "users_sessions.token", COUNT(*) FROM users_sessions WHERE CHAR_LENGTH(token) > 255
UNION ALL
SELECT "users_sessions.userIP", COUNT(*) FROM users_sessions WHERE CHAR_LENGTH(userIP) > 64
UNION ALL
SELECT "web_channels.name", COUNT(*) FROM web_channels WHERE CHAR_LENGTH(name) > 255;
-- ---------------------------------------------------------------------
-- ПРОВЕРКА 3. Нет ли дублей перед созданием UNIQUE-индексов
-- Пусто = OK.
-- ---------------------------------------------------------------------
SELECT "entries.name" AS unique_candidate, name AS value, COUNT(*) AS cnt
FROM entries GROUP BY name HAVING cnt > 1 LIMIT 10;
SELECT "entries_categories.name" AS unique_candidate, name AS value, COUNT(*) AS cnt
FROM entries_categories GROUP BY name HAVING cnt > 1 LIMIT 10;
SELECT "pages_static.name" AS unique_candidate, name AS value, COUNT(*) AS cnt
FROM pages_static GROUP BY name HAVING cnt > 1 LIMIT 10;
SELECT "users_groups.name" AS unique_candidate, name AS value, COUNT(*) AS cnt
FROM users_groups GROUP BY name HAVING cnt > 1 LIMIT 10;
SELECT "web_channels.name" AS unique_candidate, name AS value, COUNT(*) AS cnt
FROM web_channels GROUP BY name HAVING cnt > 1 LIMIT 10;
SELECT "users.login" AS unique_candidate, login AS value, COUNT(*) AS cnt
FROM users GROUP BY login HAVING cnt > 1 LIMIT 10;
SELECT "users.email" AS unique_candidate, email AS value, COUNT(*) AS cnt
FROM users GROUP BY email HAVING cnt > 1 LIMIT 10;
-- ---------------------------------------------------------------------
-- ПРОВЕРКА 4. Окружение сервера
-- ---------------------------------------------------------------------
SELECT
VERSION() AS mysql_version,
@@sql_mode AS sql_mode,
@@lower_case_table_names AS lower_case_table_names,
@@character_set_database AS charset,
@@collation_database AS collation,
@@default_storage_engine AS engine;
-- ---------------------------------------------------------------------
-- ПРОВЕРКА 5. Дубликат base_site_title в configurations (информационно)
-- Если две строки — это баг установщика, не критично.
-- ---------------------------------------------------------------------
SELECT id, name, LEFT(value, 60) AS value_preview
FROM configurations
WHERE name = "base_site_title"
ORDER BY id;
Миграция
-- =====================================================================
-- МИГРАЦИЯ CMS «ГИРВАС» 0.4.0 «Сегежа»
-- СУБД: MySQL 8.0+
-- Идемпотентно: можно запускать повторно
--
-- ВАЖНО: перед запуском сделайте бэкап:
-- mysqldump -h HOST -u USER -p DBNAME > backup_before_migration.sql
--
-- ВАЖНО: НЕ оборачивай в START TRANSACTION.
-- DDL в MySQL не транзакционен.
-- =====================================================================
SET NAMES utf8mb4;
SET @db := DATABASE();
-- =====================================================================
-- ШАГ 1. ALTER COLUMN: bigint unsigned → int для id
-- =====================================================================
-- 1.1. configurations.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "configurations"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE configurations MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: configurations.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.2. content_blocks.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "content_blocks"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE content_blocks MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: content_blocks.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.3. entries.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE entries MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: entries.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.4. entries_categories.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries_categories"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE entries_categories MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: entries_categories.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.5. entries_comments.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries_comments"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE entries_comments MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: entries_comments.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.6. entries_samples.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries_samples"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE entries_samples MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: entries_samples.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.7. forms.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "forms"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE forms MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: forms.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.8. forms_data.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "forms_data"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE forms_data MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: forms_data.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.9. metrics.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "metrics"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE metrics MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: metrics.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.10. pages_static.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "pages_static"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE pages_static MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: pages_static.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.11. reports.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "reports"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE reports MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: reports.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.12. users.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE users MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: users.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.13. users_groups.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_groups"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE users_groups MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: users_groups.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.14. users_registration_submits.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE users_registration_submits MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: users_registration_submits.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.15. users_sessions.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_sessions"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE users_sessions MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: users_sessions.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 1.16. web_channels.id
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "web_channels"
AND column_name = "id" AND column_type = "bigint unsigned"),
"ALTER TABLE web_channels MODIFY COLUMN id INT NOT NULL AUTO_INCREMENT",
"SELECT ""skip: web_channels.id already int"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- =====================================================================
-- ШАГ 2. ALTER COLUMN: text → varchar(N)
-- =====================================================================
-- 2.1. configurations.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "configurations"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE configurations MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: configurations.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.2. entries.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE entries MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: entries.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.3. entries_categories.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries_categories"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE entries_categories MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: entries_categories.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.4. entries_samples.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries_samples"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE entries_samples MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: entries_samples.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.5. forms.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "forms"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE forms MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: forms.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.6. pages_static.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "pages_static"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE pages_static MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: pages_static.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.7. users.login
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users"
AND column_name = "login" AND data_type = "text"),
"ALTER TABLE users MODIFY COLUMN login VARCHAR(255) NOT NULL",
"SELECT ""skip: users.login already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.8. users.email
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users"
AND column_name = "email" AND data_type = "text"),
"ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NOT NULL",
"SELECT ""skip: users.email already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.9. users_groups.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_groups"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE users_groups MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: users_groups.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.10. users_registration_submits.submitToken
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND column_name = "submitToken" AND data_type = "text"),
"ALTER TABLE users_registration_submits MODIFY COLUMN submitToken VARCHAR(255) NOT NULL",
"SELECT ""skip: submitToken already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.11. users_registration_submits.refusalToken
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND column_name = "refusalToken" AND data_type = "text"),
"ALTER TABLE users_registration_submits MODIFY COLUMN refusalToken VARCHAR(255) NOT NULL",
"SELECT ""skip: refusalToken already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.12. users_sessions.token
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_sessions"
AND column_name = "token" AND data_type = "text"),
"ALTER TABLE users_sessions MODIFY COLUMN token VARCHAR(255) NOT NULL",
"SELECT ""skip: users_sessions.token already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.13. users_sessions.userIP
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users_sessions"
AND column_name = "userIP" AND data_type = "text"),
"ALTER TABLE users_sessions MODIFY COLUMN userIP VARCHAR(64) NOT NULL",
"SELECT ""skip: users_sessions.userIP already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 2.14. web_channels.name
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "web_channels"
AND column_name = "name" AND data_type = "text"),
"ALTER TABLE web_channels MODIFY COLUMN name VARCHAR(255) NOT NULL",
"SELECT ""skip: web_channels.name already varchar"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- =====================================================================
-- ШАГ 3. ALTER COLUMN: text → longtext
-- =====================================================================
-- 3.1. configurations.value
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "configurations"
AND column_name = "value" AND data_type = "text"),
"ALTER TABLE configurations MODIFY COLUMN value LONGTEXT",
"SELECT ""skip: configurations.value already longtext"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3.2. entries_comments.content
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "entries_comments"
AND column_name = "content" AND data_type = "text"),
"ALTER TABLE entries_comments MODIFY COLUMN content LONGTEXT",
"SELECT ""skip: entries_comments.content already longtext"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3.3. users.passwordHash
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users"
AND column_name = "passwordHash" AND data_type = "text"),
"ALTER TABLE users MODIFY COLUMN passwordHash LONGTEXT NOT NULL",
"SELECT ""skip: users.passwordHash already longtext"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3.4. users.securityHash
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.columns
WHERE table_schema = @db AND table_name = "users"
AND column_name = "securityHash" AND data_type = "text"),
"ALTER TABLE users MODIFY COLUMN securityHash LONGTEXT NOT NULL",
"SELECT ""skip: users.securityHash already longtext"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- =====================================================================
-- ШАГ 4. DROP INDEX: удалить дублирующий UNIQUE(id)
-- =====================================================================
-- 4.1. configurations
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "configurations"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE configurations DROP INDEX id",
"SELECT ""skip: configurations.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.2. content_blocks
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "content_blocks"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE content_blocks DROP INDEX id",
"SELECT ""skip: content_blocks.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.3. entries
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE entries DROP INDEX id",
"SELECT ""skip: entries.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.4. entries_categories
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_categories"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE entries_categories DROP INDEX id",
"SELECT ""skip: entries_categories.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.5. entries_comments
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_comments"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE entries_comments DROP INDEX id",
"SELECT ""skip: entries_comments.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.6. entries_samples
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_samples"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE entries_samples DROP INDEX id",
"SELECT ""skip: entries_samples.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.7. forms
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "forms"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE forms DROP INDEX id",
"SELECT ""skip: forms.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.8. forms_data
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "forms_data"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE forms_data DROP INDEX id",
"SELECT ""skip: forms_data.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.9. metrics
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "metrics"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE metrics DROP INDEX id",
"SELECT ""skip: metrics.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.10. pages_static
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "pages_static"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE pages_static DROP INDEX id",
"SELECT ""skip: pages_static.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.11. reports
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "reports"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE reports DROP INDEX id",
"SELECT ""skip: reports.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.12. users
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE users DROP INDEX id",
"SELECT ""skip: users.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.13. users_groups
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_groups"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE users_groups DROP INDEX id",
"SELECT ""skip: users_groups.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.14. users_registration_submits
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE users_registration_submits DROP INDEX id",
"SELECT ""skip: users_registration_submits.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.15. users_sessions
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_sessions"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE users_sessions DROP INDEX id",
"SELECT ""skip: users_sessions.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 4.16. web_channels
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "web_channels"
AND index_name = "id" AND column_name = "id"),
"ALTER TABLE web_channels DROP INDEX id",
"SELECT ""skip: web_channels.id index not exists"""
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- =====================================================================
-- ШАГ 5. CREATE TABLE IF NOT EXISTS: 6 новых таблиц
-- =====================================================================
-- 5.1. oauth_clients
CREATE TABLE IF NOT EXISTS oauth_clients (
id int NOT NULL AUTO_INCREMENT,
clientID varchar(255) NOT NULL,
clientSecret longtext NOT NULL,
name varchar(255) NOT NULL,
description longtext,
redirectURI longtext NOT NULL,
grantTypes varchar(255) NOT NULL DEFAULT "authorization_code refresh_token",
scopes varchar(255) NOT NULL DEFAULT "profile email read",
userID bigint NOT NULL DEFAULT 0,
isActive tinyint(1) NOT NULL DEFAULT 1,
isVerified tinyint(1) NOT NULL DEFAULT 0,
verifiedAt bigint DEFAULT NULL,
verifiedBy bigint DEFAULT NULL,
ownerEmail varchar(255) NOT NULL DEFAULT "",
maxTokens int NOT NULL DEFAULT 100,
tokenTTL int NOT NULL DEFAULT 3600,
allowedIPs longtext,
createdUnixTimestamp int NOT NULL DEFAULT 0,
updatedUnixTimestamp int NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY idx_oauth_clients_client_id (clientID),
KEY idx_oauth_clients_user_id (userID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 5.2. oauth_auth_codes
CREATE TABLE IF NOT EXISTS oauth_auth_codes (
id int NOT NULL AUTO_INCREMENT,
code varchar(255) NOT NULL,
clientID bigint NOT NULL DEFAULT 0,
userID bigint NOT NULL DEFAULT 0,
scopes varchar(255) NOT NULL DEFAULT "",
redirectURI longtext NOT NULL,
codeChallenge longtext,
codeChallengeMethod varchar(16) NOT NULL DEFAULT "S256",
expiresAt bigint NOT NULL DEFAULT 0,
isRevoked tinyint(1) NOT NULL DEFAULT 0,
createdUnixTimestamp int NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY idx_oauth_auth_codes_code (code),
KEY idx_oauth_auth_codes_client_id (clientID),
KEY idx_oauth_auth_codes_expires_at (expiresAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 5.3. oauth_access_tokens
CREATE TABLE IF NOT EXISTS oauth_access_tokens (
id int NOT NULL AUTO_INCREMENT,
accessToken varchar(255) NOT NULL,
refreshToken varchar(255) DEFAULT NULL,
clientID bigint NOT NULL DEFAULT 0,
userID bigint NOT NULL DEFAULT 0,
scopes varchar(255) NOT NULL DEFAULT "",
expiresAt bigint NOT NULL DEFAULT 0,
isRevoked tinyint(1) NOT NULL DEFAULT 0,
revokedAt bigint DEFAULT NULL,
createdUnixTimestamp int NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY idx_oauth_access_tokens_access_token (accessToken),
UNIQUE KEY idx_oauth_access_tokens_refresh_token (refreshToken),
KEY idx_oauth_access_tokens_client_id (clientID),
KEY idx_oauth_access_tokens_user_id (userID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 5.4. pages_static_versions
CREATE TABLE IF NOT EXISTS pages_static_versions (
id int NOT NULL AUTO_INCREMENT,
pageStaticID bigint NOT NULL DEFAULT 0,
version varchar(64) NOT NULL,
locale varchar(16) NOT NULL,
texts json NOT NULL,
effectiveFrom int NOT NULL DEFAULT 0,
createdUnixTimestamp int NOT NULL DEFAULT 0,
createdByID bigint NOT NULL DEFAULT 0,
isCurrent tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY idx_pages_static_versions_unique (pageStaticID, version, locale),
KEY idx_pages_static_versions_current (pageStaticID, locale, isCurrent)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 5.5. reports_archive
CREATE TABLE IF NOT EXISTS reports_archive (
id bigint NOT NULL,
variables json DEFAULT NULL,
metadata json DEFAULT NULL,
createdUnixTimestamp int NOT NULL DEFAULT 0,
archivedUnixTimestamp int NOT NULL DEFAULT 0,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 5.6. users_consents
CREATE TABLE IF NOT EXISTS users_consents (
id int NOT NULL AUTO_INCREMENT,
userID bigint NOT NULL DEFAULT 0,
formID bigint NOT NULL DEFAULT 0,
formReportID bigint NOT NULL DEFAULT 0,
pageStaticID bigint NOT NULL DEFAULT 0,
documentVersion varchar(64) NOT NULL,
locale varchar(16) NOT NULL,
ip varchar(64) NOT NULL,
userAgent varchar(512) DEFAULT NULL,
source varchar(64) NOT NULL DEFAULT "form",
consentedAt bigint NOT NULL DEFAULT 0,
revokedAt bigint DEFAULT NULL,
revokeReason longtext,
revokedByID bigint NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_users_consents_user (userID, pageStaticID),
KEY idx_users_consents_recent (userID, pageStaticID, consentedAt),
KEY idx_users_consents_active (userID),
KEY idx_users_consents_document (pageStaticID, documentVersion),
KEY idx_users_consents_form (formID),
KEY idx_users_consents_revoked_by (revokedByID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- =====================================================================
-- ШАГ 6. CREATE INDEX: ~21 индекс на существующих таблицах
-- =====================================================================
-- 6.1. entries.idx_entries_name_unique
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries"
AND index_name = "idx_entries_name_unique"),
"SELECT ""skip: idx_entries_name_unique exists""",
"CREATE UNIQUE INDEX idx_entries_name_unique ON entries (name)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.2. entries_categories.idx_entries_categories_parent
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_categories"
AND index_name = "idx_entries_categories_parent"),
"SELECT ""skip: idx_entries_categories_parent exists""",
"CREATE INDEX idx_entries_categories_parent ON entries_categories (parentID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.3. entries_categories.idx_entries_categories_name_unique
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_categories"
AND index_name = "idx_entries_categories_name_unique"),
"SELECT ""skip: idx_entries_categories_name_unique exists""",
"CREATE UNIQUE INDEX idx_entries_categories_name_unique ON entries_categories (name)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.4. entries_comments.idx_entries_comments_entry
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_comments"
AND index_name = "idx_entries_comments_entry"),
"SELECT ""skip: idx_entries_comments_entry exists""",
"CREATE INDEX idx_entries_comments_entry ON entries_comments (entryID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.5. entries_comments.idx_entries_comments_author
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_comments"
AND index_name = "idx_entries_comments_author"),
"SELECT ""skip: idx_entries_comments_author exists""",
"CREATE INDEX idx_entries_comments_author ON entries_comments (authorID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.6. entries_comments.idx_entries_comments_created
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "entries_comments"
AND index_name = "idx_entries_comments_created"),
"SELECT ""skip: idx_entries_comments_created exists""",
"CREATE INDEX idx_entries_comments_created ON entries_comments (createdUnixTimestamp DESC)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.7. forms_data.idx_forms_data_form
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "forms_data"
AND index_name = "idx_forms_data_form"),
"SELECT ""skip: idx_forms_data_form exists""",
"CREATE INDEX idx_forms_data_form ON forms_data (formID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.8. forms_data.idx_forms_data_created
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "forms_data"
AND index_name = "idx_forms_data_created"),
"SELECT ""skip: idx_forms_data_created exists""",
"CREATE INDEX idx_forms_data_created ON forms_data (createdUnixTimestamp DESC)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.9. metrics.idx_metrics_date
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "metrics"
AND index_name = "idx_metrics_date"),
"SELECT ""skip: idx_metrics_date exists""",
"CREATE INDEX idx_metrics_date ON metrics (date)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.10. pages_static.idx_pages_static_author
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "pages_static"
AND index_name = "idx_pages_static_author"),
"SELECT ""skip: idx_pages_static_author exists""",
"CREATE INDEX idx_pages_static_author ON pages_static (authorID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.11. pages_static.idx_pages_static_created
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "pages_static"
AND index_name = "idx_pages_static_created"),
"SELECT ""skip: idx_pages_static_created exists""",
"CREATE INDEX idx_pages_static_created ON pages_static (createdUnixTimestamp DESC)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.12. pages_static.idx_pages_static_name_unique
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "pages_static"
AND index_name = "idx_pages_static_name_unique"),
"SELECT ""skip: idx_pages_static_name_unique exists""",
"CREATE UNIQUE INDEX idx_pages_static_name_unique ON pages_static (name)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.13. reports.idx_reports_created
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "reports"
AND index_name = "idx_reports_created"),
"SELECT ""skip: idx_reports_created exists""",
"CREATE INDEX idx_reports_created ON reports (createdUnixTimestamp)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.14. users.idx_users_login_unique
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users"
AND index_name = "idx_users_login_unique"),
"SELECT ""skip: idx_users_login_unique exists""",
"CREATE UNIQUE INDEX idx_users_login_unique ON users (login)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.15. users.idx_users_email_unique
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users"
AND index_name = "idx_users_email_unique"),
"SELECT ""skip: idx_users_email_unique exists""",
"CREATE UNIQUE INDEX idx_users_email_unique ON users (email)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.16. users_sessions.idx_users_sessions_user
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_sessions"
AND index_name = "idx_users_sessions_user"),
"SELECT ""skip: idx_users_sessions_user exists""",
"CREATE INDEX idx_users_sessions_user ON users_sessions (userID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.17. users_sessions.idx_users_sessions_token
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_sessions"
AND index_name = "idx_users_sessions_token"),
"SELECT ""skip: idx_users_sessions_token exists""",
"CREATE INDEX idx_users_sessions_token ON users_sessions (token)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.18. web_channels.idx_web_channels_name_unique
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "web_channels"
AND index_name = "idx_web_channels_name_unique"),
"SELECT ""skip: idx_web_channels_name_unique exists""",
"CREATE UNIQUE INDEX idx_web_channels_name_unique ON web_channels (name)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.19. users_registration_submits.idx_users_registration_submits_user
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND index_name = "idx_users_registration_submits_user"),
"SELECT ""skip: idx_users_registration_submits_user exists""",
"CREATE INDEX idx_users_registration_submits_user ON users_registration_submits (userID)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.20. users_registration_submits.idx_users_registration_submits_submit_token
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND index_name = "idx_users_registration_submits_submit_token"),
"SELECT ""skip: idx_users_registration_submits_submit_token exists""",
"CREATE INDEX idx_users_registration_submits_submit_token ON users_registration_submits (submitToken)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 6.21. users_registration_submits.idx_users_registration_submits_refusal_token
SET @sql := IF(
EXISTS(SELECT 1 FROM information_schema.statistics
WHERE table_schema = @db AND table_name = "users_registration_submits"
AND index_name = "idx_users_registration_submits_refusal_token"),
"SELECT ""skip: idx_users_registration_submits_refusal_token exists""",
"CREATE INDEX idx_users_registration_submits_refusal_token ON users_registration_submits (refusalToken)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
Завершающая проверка
-- =====================================================================
-- POST-CHECK после миграции CMS «ГИРВАС» 0.4.0 «Сегежа»
-- СУБД: MySQL 8.0+
-- Запускать ПОСЛЕ миграции (02-migration.sql)
-- =====================================================================
-- База определяется автоматически: используется DATABASE()
-- (та БД, к которой вы подключены в текущей сессии).
-- =====================================================================
SET NAMES utf8mb4;
-- ---------------------------------------------------------------------
-- 0. Диагностика: куда смотрит скрипт
-- ---------------------------------------------------------------------
SELECT
DATABASE() AS current_database,
"OK: проверки идут по текущей БД" AS status;
-- ---------------------------------------------------------------------
-- 1. Все ли таблицы на месте? Пусто = OK.
-- ---------------------------------------------------------------------
SELECT "MISSING_TABLE" AS problem, t.name AS detail
FROM (
SELECT "configurations" AS name UNION ALL
SELECT "content_blocks" UNION ALL
SELECT "entries" UNION ALL
SELECT "entries_categories" UNION ALL
SELECT "entries_comments" UNION ALL
SELECT "entries_samples" UNION ALL
SELECT "forms" UNION ALL
SELECT "forms_data" UNION ALL
SELECT "metrics" UNION ALL
SELECT "oauth_access_tokens" UNION ALL
SELECT "oauth_auth_codes" UNION ALL
SELECT "oauth_clients" UNION ALL
SELECT "pages_static" UNION ALL
SELECT "pages_static_versions" UNION ALL
SELECT "reports" UNION ALL
SELECT "reports_archive" UNION ALL
SELECT "users" UNION ALL
SELECT "users_consents" UNION ALL
SELECT "users_groups" UNION ALL
SELECT "users_registration_submits" UNION ALL
SELECT "users_sessions" UNION ALL
SELECT "web_channels"
) AS t
LEFT JOIN information_schema.tables it
ON it.table_schema = DATABASE() AND it.table_name = t.name
WHERE it.table_name IS NULL;
-- ---------------------------------------------------------------------
-- 2. Остались ли bigint unsigned на id? Пусто = OK.
-- ---------------------------------------------------------------------
SELECT "LEFTOVER_BIGINT_ID" AS problem, table_name, column_type
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND column_name = "id"
AND column_type = "bigint unsigned"
ORDER BY table_name;
-- ---------------------------------------------------------------------
-- 3. Остались ли UNIQUE(id)? Пусто = OK.
-- ---------------------------------------------------------------------
SELECT "LEFTOVER_UNIQUE_ID" AS problem, table_name, index_name
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND index_name = "id"
AND column_name = "id"
ORDER BY table_name;
-- ---------------------------------------------------------------------
-- 4. Все ли колонки text → varchar? Пусто = OK.
-- ---------------------------------------------------------------------
SELECT "LEFTOVER_TEXT" AS problem, table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND data_type = "text"
AND (
(table_name = "configurations" AND column_name = "name") OR
(table_name = "entries" AND column_name = "name") OR
(table_name = "entries_categories" AND column_name = "name") OR
(table_name = "entries_samples" AND column_name = "name") OR
(table_name = "forms" AND column_name = "name") OR
(table_name = "pages_static" AND column_name = "name") OR
(table_name = "users_groups" AND column_name = "name") OR
(table_name = "web_channels" AND column_name = "name") OR
(table_name = "users" AND column_name IN ("login", "email")) OR
(table_name = "users_registration_submits" AND column_name IN ("submitToken", "refusalToken")) OR
(table_name = "users_sessions" AND column_name IN ("token", "userIP"))
)
ORDER BY table_name, column_name;
-- ---------------------------------------------------------------------
-- 5. Все ли longtext там, где надо? Пусто = OK.
-- ---------------------------------------------------------------------
SELECT "NOT_LONGTEXT" AS problem, table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND (
(table_name = "configurations" AND column_name = "value" AND data_type != "longtext") OR
(table_name = "entries_comments" AND column_name = "content" AND data_type != "longtext") OR
(table_name = "users" AND column_name = "passwordHash" AND data_type != "longtext") OR
(table_name = "users" AND column_name = "securityHash" AND data_type != "longtext")
)
ORDER BY table_name, column_name;
-- ---------------------------------------------------------------------
-- 6. AUTO_INCREMENT на месте?
-- ---------------------------------------------------------------------
SELECT table_name, auto_increment
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_type = "BASE TABLE"
AND auto_increment IS NOT NULL
ORDER BY table_name;
-- ---------------------------------------------------------------------
-- 7. Итоговый список индексов
-- ---------------------------------------------------------------------
SELECT table_name, index_name, non_unique,
GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols
FROM information_schema.statistics
WHERE table_schema = DATABASE()
GROUP BY table_name, index_name, non_unique
ORDER BY table_name, index_name;
-- ---------------------------------------------------------------------
-- 8. Финальный список таблиц + engine + collation
-- ---------------------------------------------------------------------
SELECT table_name, engine, table_collation
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_type = "BASE TABLE"
ORDER BY table_name;
Миграция PostgreSQL
Перед запуском убедитесь, что данные влезают в новые типы. Если в вашей БД есть значения длиннее 255 символов в колонках name, login, email — миграция упадёт. Транзакция откатится целиком, ничего не сломается, но вы получите ошибку.
Предварительная проверка
SELECT "configurations.name" AS col, COUNT(*) AS too_long
FROM configurations WHERE length(name) > 255
UNION ALL SELECT "content_blocks.name", COUNT(*) FROM content_blocks WHERE length(name) > 255
UNION ALL SELECT "entries.name", COUNT(*) FROM entries WHERE length(name) > 255
UNION ALL SELECT "entries_categories.name", COUNT(*) FROM entries_categories WHERE length(name) > 255
UNION ALL SELECT "entries_samples.name", COUNT(*) FROM entries_samples WHERE length(name) > 255
UNION ALL SELECT "forms.name", COUNT(*) FROM forms WHERE length(name) > 255
UNION ALL SELECT "pages_static.name", COUNT(*) FROM pages_static WHERE length(name) > 255
UNION ALL SELECT "users.login", COUNT(*) FROM users WHERE length(login) > 255
UNION ALL SELECT "users.email", COUNT(*) FROM users WHERE length(email) > 255
UNION ALL SELECT "users_groups.name", COUNT(*) FROM users_groups WHERE length(name) > 255
UNION ALL SELECT "users_registration_submits.submitToken", COUNT(*) FROM users_registration_submits WHERE length("submitToken") > 255
UNION ALL SELECT "users_registration_submits.refusalToken", COUNT(*) FROM users_registration_submits WHERE length("refusalToken") > 255
UNION ALL SELECT "users_sessions.token", COUNT(*) FROM users_sessions WHERE length(token) > 255
UNION ALL SELECT "users_sessions.userIP", COUNT(*) FROM users_sessions WHERE length("userIP") > 64
UNION ALL SELECT "web_channels.name", COUNT(*) FROM web_channels WHERE length(name) > 255;
BEGIN;
-- ---------------------------------------------------------------------
-- ШАГ 1. ALTER COLUMN TYPE: text → varchar(N)
-- В PG ALTER TYPE пересобирает зависимые индексы автоматически.
-- ---------------------------------------------------------------------
ALTER TABLE public.configurations
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.content_blocks
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.entries
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.entries_categories
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.entries_samples
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.forms
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.pages_static
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.users
ALTER COLUMN login TYPE varchar(255);
ALTER TABLE public.users
ALTER COLUMN email TYPE varchar(255);
ALTER TABLE public.users_groups
ALTER COLUMN name TYPE varchar(255);
ALTER TABLE public.users_registration_submits
ALTER COLUMN "submitToken" TYPE varchar(255);
ALTER TABLE public.users_registration_submits
ALTER COLUMN "refusalToken" TYPE varchar(255);
ALTER TABLE public.users_sessions
ALTER COLUMN token TYPE varchar(255);
ALTER TABLE public.users_sessions
ALTER COLUMN "userIP" TYPE varchar(64);
ALTER TABLE public.web_channels
ALTER COLUMN name TYPE varchar(255);
-- ---------------------------------------------------------------------
-- ШАГ 2. Новые индексы для существующих таблиц
-- ---------------------------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_entries_updated
ON public.entries USING btree ("updatedUnixTimestamp");
CREATE INDEX IF NOT EXISTS idx_forms_data_created
ON public.forms_data USING btree ("createdUnixTimestamp" DESC);
CREATE INDEX IF NOT EXISTS idx_forms_data_form
ON public.forms_data USING btree ("formID");
CREATE INDEX IF NOT EXISTS idx_metrics_date
ON public.metrics USING btree (date);
CREATE INDEX IF NOT EXISTS idx_pages_static_author
ON public.pages_static USING btree ("authorID");
CREATE INDEX IF NOT EXISTS idx_pages_static_created
ON public.pages_static USING btree ("createdUnixTimestamp" DESC);
CREATE UNIQUE INDEX IF NOT EXISTS idx_pages_static_name_unique
ON public.pages_static USING btree (name);
CREATE INDEX IF NOT EXISTS idx_pages_static_texts_gin
ON public.pages_static USING gin (texts);
CREATE INDEX IF NOT EXISTS idx_reports_created
ON public.reports USING btree ("createdUnixTimestamp");
CREATE INDEX IF NOT EXISTS idx_users_sessions_token
ON public.users_sessions USING btree (token);
CREATE INDEX IF NOT EXISTS idx_users_sessions_user
ON public.users_sessions USING btree ("userID");
CREATE UNIQUE INDEX IF NOT EXISTS idx_web_channels_name_unique
ON public.web_channels USING btree (name);
-- ---------------------------------------------------------------------
-- ШАГ 3. Новые таблицы
-- ---------------------------------------------------------------------
CREATE SEQUENCE IF NOT EXISTS oauth_clients_id_seq AS integer START 1 INCREMENT 1;
CREATE SEQUENCE IF NOT EXISTS oauth_access_tokens_id_seq AS integer START 1 INCREMENT 1;
CREATE SEQUENCE IF NOT EXISTS oauth_auth_codes_id_seq AS integer START 1 INCREMENT 1;
CREATE SEQUENCE IF NOT EXISTS pages_static_versions_id_seq AS integer START 1 INCREMENT 1;
CREATE SEQUENCE IF NOT EXISTS users_consents_id_seq AS integer START 1 INCREMENT 1;
-- 3.1. OAuth: клиенты
CREATE TABLE IF NOT EXISTS public.oauth_clients (
id integer NOT NULL DEFAULT nextval("oauth_clients_id_seq"::regclass),
"clientID" varchar(255) NOT NULL,
"clientSecret" text NOT NULL,
name varchar(255) NOT NULL,
description text,
"redirectURI" text NOT NULL,
"grantTypes" varchar(255) NOT NULL DEFAULT "authorization_code refresh_token"::character varying,
scopes varchar(255) NOT NULL DEFAULT "profile email read"::character varying,
"userID" bigint NOT NULL DEFAULT 0,
"isActive" boolean NOT NULL DEFAULT true,
"isVerified" boolean NOT NULL DEFAULT false,
"verifiedAt" bigint DEFAULT 0,
"verifiedBy" bigint DEFAULT 0,
"ownerEmail" varchar(255) NOT NULL DEFAULT ""::character varying,
"maxTokens" integer NOT NULL DEFAULT 100,
"tokenTTL" integer NOT NULL DEFAULT 3600,
"allowedIPs" text,
"createdUnixTimestamp" integer NOT NULL DEFAULT 0,
"updatedUnixTimestamp" integer NOT NULL DEFAULT 0,
CONSTRAINT oauth_clients_pkey PRIMARY KEY (id)
);
-- 3.2. OAuth: токены доступа
CREATE TABLE IF NOT EXISTS public.oauth_access_tokens (
id integer NOT NULL DEFAULT nextval("oauth_access_tokens_id_seq"::regclass),
"accessToken" varchar(255) NOT NULL,
"refreshToken" varchar(255),
"clientID" bigint NOT NULL DEFAULT 0,
"userID" bigint NOT NULL DEFAULT 0,
scopes varchar(255) NOT NULL DEFAULT ""::character varying,
"expiresAt" bigint NOT NULL DEFAULT 0,
"isRevoked" boolean NOT NULL DEFAULT false,
"revokedAt" bigint DEFAULT 0,
"createdUnixTimestamp" integer NOT NULL DEFAULT 0,
CONSTRAINT oauth_access_tokens_pkey PRIMARY KEY (id)
);
-- 3.3. OAuth: коды авторизации
CREATE TABLE IF NOT EXISTS public.oauth_auth_codes (
id integer NOT NULL DEFAULT nextval("oauth_auth_codes_id_seq"::regclass),
code varchar(255) NOT NULL,
"clientID" bigint NOT NULL DEFAULT 0,
"userID" bigint NOT NULL DEFAULT 0,
scopes varchar(255) NOT NULL DEFAULT ""::character varying,
"redirectURI" text NOT NULL,
"codeChallenge" text,
"codeChallengeMethod" varchar(16) NOT NULL DEFAULT "S256"::character varying,
"expiresAt" bigint NOT NULL DEFAULT 0,
"isRevoked" boolean NOT NULL DEFAULT false,
"createdUnixTimestamp" integer NOT NULL DEFAULT 0,
CONSTRAINT oauth_auth_codes_pkey PRIMARY KEY (id)
);
-- 3.4. Версии статических страниц (152-ФЗ)
CREATE TABLE IF NOT EXISTS public.pages_static_versions (
id integer NOT NULL DEFAULT nextval("pages_static_versions_id_seq"::regclass),
"pageStaticID" bigint NOT NULL DEFAULT 0,
version varchar(64) NOT NULL,
locale varchar(16) NOT NULL,
texts jsonb NOT NULL,
"effectiveFrom" integer NOT NULL DEFAULT 0,
"createdUnixTimestamp" integer NOT NULL DEFAULT 0,
"createdByID" bigint NOT NULL DEFAULT 0,
"isCurrent" boolean NOT NULL DEFAULT false,
CONSTRAINT pages_static_versions_pkey PRIMARY KEY (id)
);
-- 3.5. Архив отчётов
CREATE TABLE IF NOT EXISTS public.reports_archive (
id bigint NOT NULL,
variables jsonb,
metadata jsonb,
"createdUnixTimestamp" integer NOT NULL DEFAULT 0,
"archivedUnixTimestamp" integer NOT NULL DEFAULT 0,
CONSTRAINT reports_archive_pkey PRIMARY KEY (id)
);
-- sequence у reports_archive НЕТ — id копируется из reports.
-- 3.6. Согласия пользователей (152-ФЗ)
CREATE TABLE IF NOT EXISTS public.users_consents (
id integer NOT NULL DEFAULT nextval("users_consents_id_seq"::regclass),
"userID" bigint NOT NULL DEFAULT 0,
"formID" bigint NOT NULL DEFAULT 0,
"formReportID" bigint NOT NULL DEFAULT 0,
"pageStaticID" bigint NOT NULL DEFAULT 0,
"documentVersion" varchar(64) NOT NULL,
locale varchar(16) NOT NULL,
ip varchar(64) NOT NULL,
"userAgent" varchar(512),
source varchar(64) NOT NULL DEFAULT "form"::character varying,
"consentedAt" bigint NOT NULL DEFAULT 0,
"revokedAt" bigint DEFAULT 0,
"revokeReason" text,
"revokedByID" bigint NOT NULL DEFAULT 0,
CONSTRAINT users_consents_pkey PRIMARY KEY (id)
);
-- ---------------------------------------------------------------------
-- ШАГ 4. Индексы новых таблиц
-- ---------------------------------------------------------------------
-- oauth_clients
CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_clients_client_id
ON public.oauth_clients USING btree ("clientID");
CREATE INDEX IF NOT EXISTS idx_oauth_clients_user_id
ON public.oauth_clients USING btree ("userID");
-- oauth_access_tokens
CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_access_tokens_access_token
ON public.oauth_access_tokens USING btree ("accessToken");
CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_access_tokens_refresh_token
ON public.oauth_access_tokens USING btree ("refreshToken");
CREATE INDEX IF NOT EXISTS idx_oauth_access_tokens_client_id
ON public.oauth_access_tokens USING btree ("clientID");
CREATE INDEX IF NOT EXISTS idx_oauth_access_tokens_user_id
ON public.oauth_access_tokens USING btree ("userID");
-- oauth_auth_codes
CREATE UNIQUE INDEX IF NOT EXISTS idx_oauth_auth_codes_code
ON public.oauth_auth_codes USING btree (code);
CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_client_id
ON public.oauth_auth_codes USING btree ("clientID");
CREATE INDEX IF NOT EXISTS idx_oauth_auth_codes_expires_at
ON public.oauth_auth_codes USING btree ("expiresAt");
-- pages_static_versions
CREATE UNIQUE INDEX IF NOT EXISTS idx_pages_static_versions_unique
ON public.pages_static_versions USING btree ("pageStaticID", version, locale);
CREATE INDEX IF NOT EXISTS idx_pages_static_versions_current
ON public.pages_static_versions USING btree ("pageStaticID", locale, "isCurrent");
CREATE INDEX IF NOT EXISTS idx_pages_static_versions_texts_gin
ON public.pages_static_versions USING gin (texts);
-- users_consents
CREATE INDEX IF NOT EXISTS idx_users_consents_user
ON public.users_consents USING btree ("userID", "pageStaticID");
CREATE INDEX IF NOT EXISTS idx_users_consents_document
ON public.users_consents USING btree ("pageStaticID", "documentVersion");
CREATE INDEX IF NOT EXISTS idx_users_consents_form
ON public.users_consents USING btree ("formID");
CREATE INDEX IF NOT EXISTS idx_users_consents_recent
ON public.users_consents USING btree ("userID", "pageStaticID", "consentedAt");
CREATE INDEX IF NOT EXISTS idx_users_consents_active
ON public.users_consents USING btree ("userID") WHERE ("revokedAt" IS NULL);
CREATE INDEX IF NOT EXISTS idx_users_consents_revoked_by
ON public.users_consents USING btree ("revokedByID") WHERE ("revokedByID" > 0);
COMMIT;
Альтернатива: переустановка системы
Если ваш сайт не содержит важных данных — или вы готовы их потерять — можно вместо миграции переустановить CMS. Это гарантирует корректную схему базы данных без рисков, связанных с миграцией.
Когда это уместно:
- сайт находится на стадии разработки или тестирования;
- контент можно легко восстановить или пересоздать;
- вы хотите начать с «чистого листа» с новой версией CMS.
Когда НЕ подходит:
- боевой сайт с контентом, пользователями, формами, отчётами;
- если есть данные, которые нельзя восстановить;
- если нет возможности выгрузить контент перед переустановкой.
Порядок переустановки:
- Экспортируйте контент, который хотите сохранить: записи, страницы, пользователей, формы. Это можно сделать через административную панель, либо вручную — через SQL.
- Сделайте бэкап всей базы данных — на случай, если передумаете.
- Удалите файлы CMS из корневой директории.
- Удалите базу данных целиком (или создайте новую пустую с тем же именем).
- Распакуйте новую версию CMS.
- Пройдите установку заново: заполните данные о домене, БД, создайте администратора.
- Импортируйте ранее выгруженный контент.
- Очистите кэш:
rm -R ./cache/*.
Внимание: переустановка приводит к полной потере данных, если их не выгрузить заранее. Не используйте этот путь для «боевых» сайтов без предварительного экспорта контента.
Заключение
Обновление 0.4.0 «Сегежа» для CMS «ГИРВАС» является самым масштабным за всю историю проекта. Оно объединяет несколько стратегических направлений: полное техническое покрытие требований 152-ФЗ на уровне ядра, закладку фундамента для OAuth-провайдинга, внедрение слоя SQL-диалектов с поддержкой MySQL и PostgreSQL, мультиязычность настроек сайта и ряд улучшений административной панели.
Мы рекомендуем всем пользователям выполнить обновление, чтобы оценить возросшие возможности системы. Отдельно стоит отметить направления, которые определяют развитие продукта на перспективу: соответствие требованиям законодательства о персональных данных и поддержка нескольких СУБД. Это не разовые улучшения, а системная работа над тем, чтобы «ГИРВАС» оставался современным, безопасным и востребованным инструментом для сайтов любого масштаба — от небольшого блога до корпоративного портала.
Комментарии