ваше сообщение коммита

This commit is contained in:
2025-07-27 03:30:13 +03:00
parent 057fe6254c
commit 1835632be9
141 changed files with 32514 additions and 6661 deletions

View File

@@ -4,9 +4,8 @@ CREATE TABLE IF NOT EXISTS roles (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Добавляем базовые роли
INSERT INTO roles (name) VALUES ('admin'), ('user')
ON CONFLICT (name) DO NOTHING;
-- Добавляем базовые роли (пропускаем, так как таблица уже зашифрована)
-- Роли будут добавлены через encryptedDatabaseService
-- Добавляем связь пользователей с ролями
DO $$

View File

@@ -18,11 +18,6 @@ BEGIN
CREATE INDEX idx_user_identities_user_id ON user_identities(user_id);
END IF;
-- Индекс для provider и provider_id
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_identities' AND indexname = 'idx_user_identities_type_value'
) THEN
CREATE INDEX idx_user_identities_type_value ON user_identities(provider, provider_id);
END IF;
-- Индекс для provider и provider_id (пропускаем, так как колонки зашифрованы)
-- Индекс будет создан автоматически при необходимости
END $$;

View File

@@ -14,8 +14,8 @@ CREATE TABLE IF NOT EXISTS messages (
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_sender_type ON messages(sender_type);
-- CREATE INDEX IF NOT EXISTS idx_messages_sender_type ON messages(sender_type); -- пропускаем, колонка зашифрована
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel);
CREATE INDEX IF NOT EXISTS idx_messages_metadata ON messages USING gin(metadata);
-- CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel); -- пропускаем, колонка зашифрована
-- CREATE INDEX IF NOT EXISTS idx_messages_metadata ON messages USING gin(metadata); -- пропускаем, колонки нет
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);

View File

@@ -7,15 +7,16 @@ CREATE TABLE IF NOT EXISTS guest_messages (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'guest_messages' AND indexname = 'idx_guest_messages_guest_id'
) THEN
CREATE INDEX idx_guest_messages_guest_id ON guest_messages(guest_id);
END IF;
END $$;
-- DO $$
-- BEGIN
-- IF NOT EXISTS (
-- SELECT 1 FROM pg_indexes
-- WHERE tablename = 'guest_messages' AND indexname = 'idx_guest_messages_guest_id'
-- ) THEN
-- CREATE INDEX idx_guest_messages_guest_id ON guest_messages(guest_id);
-- END IF;
-- END $$;
-- -- Пропускаем создание индекса, так как колонка guest_id зашифрована
DO $$
BEGIN

View File

@@ -22,25 +22,26 @@ END $$;
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
-- Базовые настройки
DO $$
BEGIN
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
SELECT id, 'language', 'ru', '{"available": ["ru", "en"]}'::jsonb
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_preferences
WHERE preference_key = 'language' AND user_id = u.id
);
END $$;
-- Базовые настройки (пропускаем, так как колонки зашифрованы)
-- Данные будут добавлены через encryptedDatabaseService
-- DO $$
-- BEGIN
-- INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
-- SELECT id, 'language', 'ru', '{"available": ["ru", "en"]}'::jsonb
-- FROM users u
-- WHERE NOT EXISTS (
-- SELECT 1 FROM user_preferences
-- WHERE preference_key = 'language' AND user_id = u.id
-- );
-- END $$;
DO $$
BEGIN
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
SELECT id, 'notifications', 'true', '{"channels": ["email", "telegram"]}'::jsonb
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_preferences
WHERE preference_key = 'notifications' AND user_id = u.id
);
END $$;
-- DO $$
-- BEGIN
-- INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
-- SELECT id, 'notifications', 'true', '{"channels": ["email", "telegram"]}'::jsonb
-- FROM users u
-- WHERE NOT EXISTS (
-- SELECT 1 FROM user_preferences
-- WHERE preference_key = 'notifications' AND user_id = u.id
-- );
-- END $$;

View File

@@ -6,8 +6,8 @@ CREATE TABLE IF NOT EXISTS nonces (
created_at TIMESTAMP DEFAULT NOW()
);
-- Индекс для быстрого поиска по identity_value
CREATE INDEX IF NOT EXISTS idx_nonces_identity_value ON nonces(identity_value);
-- Индекс для быстрого поиска по identity_value (пропускаем, колонка зашифрована)
-- CREATE INDEX IF NOT EXISTS idx_nonces_identity_value ON nonces(identity_value);
-- Индекс для очистки просроченных nonce
CREATE INDEX IF NOT EXISTS idx_nonces_expires_at ON nonces(expires_at);

View File

@@ -59,10 +59,13 @@ END;
$$ LANGUAGE plpgsql;
-- Создаем триггер
CREATE TRIGGER check_admin_role_trigger
AFTER INSERT OR UPDATE ON user_identities
FOR EACH ROW
EXECUTE FUNCTION check_admin_role();
-- CREATE TRIGGER check_admin_role_trigger
-- AFTER INSERT OR UPDATE ON user_identities
-- FOR EACH ROW
-- EXECUTE FUNCTION check_admin_role();
-- Триггер отключен, так как проверка роли админа происходит в JavaScript коде
-- и триггер вызывает ошибку с зашифрованными полями provider_encrypted
-- Сбрасываем все роли на user
UPDATE users SET role = 'user'::user_role;

View File

@@ -9,8 +9,8 @@ BEGIN
END IF;
END $$;
-- Удаляем гостевые идентификаторы из user_identities
DELETE FROM user_identities WHERE provider = 'guest';
-- Удаляем гостевые идентификаторы из user_identities (пропускаем, колонка зашифрована)
-- DELETE FROM user_identities WHERE provider = 'guest';
-- Удаляем индекс для guest_message_id если он существует
DO $$

View File

@@ -10,9 +10,9 @@ CREATE TABLE IF NOT EXISTS verification_codes (
used BOOLEAN DEFAULT FALSE
);
-- Индексы для оптимизации
CREATE INDEX IF NOT EXISTS idx_verification_codes_code ON verification_codes(code);
CREATE INDEX IF NOT EXISTS idx_verification_codes_provider ON verification_codes(provider);
-- Индексы для оптимизации (пропускаем зашифрованные колонки)
-- CREATE INDEX IF NOT EXISTS idx_verification_codes_code ON verification_codes(code); -- колонка зашифрована
-- CREATE INDEX IF NOT EXISTS idx_verification_codes_provider ON verification_codes(provider); -- колонка зашифрована
CREATE INDEX IF NOT EXISTS idx_verification_codes_expires ON verification_codes(expires_at);
-- Удаляем старую таблицу email_auth_tokens

View File

@@ -12,24 +12,24 @@ CREATE TABLE IF NOT EXISTS guest_user_mapping (
);
-- 2. Создание индексов для guest_user_mapping
CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_guest_id ON guest_user_mapping(guest_id);
-- CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_guest_id ON guest_user_mapping(guest_id); -- колонка зашифрована
CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_user_id ON guest_user_mapping(user_id);
-- 3. Перенос гостевых идентификаторов из user_identities в guest_user_mapping
DO $$
BEGIN
-- Выполняем только если есть гостевые идентификаторы в user_identities
IF EXISTS (SELECT 1 FROM user_identities WHERE provider = 'guest') THEN
INSERT INTO guest_user_mapping (user_id, guest_id, processed)
SELECT user_id, provider_id, true
FROM user_identities
WHERE provider = 'guest'
ON CONFLICT (guest_id) DO NOTHING;
-- Удаляем перенесенные идентификаторы
DELETE FROM user_identities WHERE provider = 'guest';
END IF;
END $$;
-- 3. Перенос гостевых идентификаторов из user_identities в guest_user_mapping (пропускаем, колонки зашифрованы)
-- DO $$
-- BEGIN
-- -- Выполняем только если есть гостевые идентификаторы в user_identities
-- IF EXISTS (SELECT 1 FROM user_identities WHERE provider = 'guest') THEN
-- INSERT INTO guest_user_mapping (user_id, guest_id, processed)
-- SELECT user_id, provider_id, true
-- FROM user_identities
-- WHERE provider = 'guest'
-- ON CONFLICT (guest_id) DO NOTHING;
--
-- -- Удаляем перенесенные идентификаторы
-- DELETE FROM user_identities WHERE provider = 'guest';
-- END IF;
-- END $$;
-- 4. Добавление/обновление поля user_id в таблице messages
DO $$
@@ -71,74 +71,74 @@ BEFORE INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION set_message_user_id();
-- 7. Перенос идентификаторов из полей users в user_identities
DO $$
DECLARE
user_rec RECORD;
BEGIN
-- Обрабатываем email
FOR user_rec IN
SELECT id, email FROM users
WHERE email IS NOT NULL AND email != ''
LOOP
-- Проверяем, существует ли такой email в user_identities
IF NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = user_rec.id AND provider = 'email' AND provider_id = user_rec.email
) THEN
-- Если нет, добавляем его
INSERT INTO user_identities (user_id, provider, provider_id)
VALUES (user_rec.id, 'email', LOWER(user_rec.email));
END IF;
END LOOP;
-- Обрабатываем address (wallet)
FOR user_rec IN
SELECT id, address FROM users
WHERE address IS NOT NULL AND address != ''
LOOP
-- Проверяем, существует ли такой адрес в user_identities
IF NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = user_rec.id AND provider = 'wallet' AND provider_id = LOWER(user_rec.address)
) THEN
-- Если нет, добавляем его
INSERT INTO user_identities (user_id, provider, provider_id)
VALUES (user_rec.id, 'wallet', LOWER(user_rec.address));
END IF;
END LOOP;
END $$;
-- 7. Перенос идентификаторов из полей users в user_identities (пропускаем, колонки зашифрованы)
-- DO $$
-- DECLARE
-- user_rec RECORD;
-- BEGIN
-- -- Обрабатываем email
-- FOR user_rec IN
-- SELECT id, email FROM users
-- WHERE email IS NOT NULL AND email != ''
-- LOOP
-- -- Проверяем, существует ли такой email в user_identities
-- IF NOT EXISTS (
-- SELECT 1 FROM user_identities
-- WHERE user_id = user_rec.id AND provider = 'email' AND provider_id = user_rec.email
-- ) THEN
-- -- Если нет, добавляем его
-- INSERT INTO user_identities (user_id, provider, provider_id)
-- VALUES (user_rec.id, 'email', LOWER(user_rec.email));
-- END IF;
-- END LOOP;
--
-- -- Обрабатываем address (wallet)
-- FOR user_rec IN
-- SELECT id, address FROM users
-- WHERE address IS NOT NULL AND address != ''
-- LOOP
-- -- Проверяем, существует ли такой адрес в user_identities
-- IF NOT EXISTS (
-- SELECT 1 FROM user_identities
-- WHERE user_id = user_rec.id AND provider = 'wallet' AND provider_id = LOWER(user_rec.address)
-- ) THEN
-- -- Если нет, добавляем его
-- INSERT INTO user_identities (user_id, provider, provider_id)
-- VALUES (user_rec.id, 'wallet', LOWER(user_rec.address));
-- END IF;
-- END LOOP;
-- END $$;
-- 8. Очистка устаревших полей в таблице users
UPDATE users
SET
email = NULL,
address = NULL,
username = NULL
WHERE
email IS NOT NULL OR address IS NOT NULL OR username IS NOT NULL;
-- 8. Очистка устаревших полей в таблице users (пропускаем, колонки зашифрованы)
-- UPDATE users
-- SET
-- email = NULL,
-- address = NULL,
-- username = NULL
-- WHERE
-- email IS NOT NULL OR address IS NOT NULL OR username IS NOT NULL;
-- 9. Нормализация регистра для email и wallet идентификаторов
UPDATE user_identities
SET provider_id = LOWER(provider_id)
WHERE (provider = 'wallet' OR provider = 'email') AND provider_id != LOWER(provider_id);
-- 9. Нормализация регистра для email и wallet идентификаторов (пропускаем, колонки зашифрованы)
-- UPDATE user_identities
-- SET provider_id = LOWER(provider_id)
-- WHERE (provider = 'wallet' OR provider = 'email') AND provider_id != LOWER(provider_id);
-- 10. Ограничения для предотвращения использования guest в user_identities
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_not_guest;
ALTER TABLE user_identities ADD CONSTRAINT check_provider_not_guest
CHECK (provider != 'guest');
-- 10. Ограничения для предотвращения использования guest в user_identities (пропускаем, колонки зашифрованы)
-- ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_not_guest;
-- ALTER TABLE user_identities ADD CONSTRAINT check_provider_not_guest
-- CHECK (provider != 'guest');
-- 11. Ограничение на допустимые типы идентификаторов
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_allowed;
ALTER TABLE user_identities ADD CONSTRAINT check_provider_allowed
CHECK (provider IN ('email', 'wallet', 'telegram'));
-- 11. Ограничение на допустимые типы идентификаторов (пропускаем, колонки зашифрованы)
-- ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_allowed;
-- ALTER TABLE user_identities ADD CONSTRAINT check_provider_allowed
-- CHECK (provider IN ('email', 'wallet', 'telegram'));
-- 12. Помечаем обработанные гостевые идентификаторы
UPDATE guest_user_mapping
SET processed = true
WHERE processed = false AND NOT EXISTS (
SELECT 1 FROM guest_messages WHERE guest_id = guest_user_mapping.guest_id
);
-- 12. Помечаем обработанные гостевые идентификаторы (пропускаем, колонки зашифрованы)
-- UPDATE guest_user_mapping
-- SET processed = true
-- WHERE processed = false AND NOT EXISTS (
-- SELECT 1 FROM guest_messages WHERE guest_id = guest_user_mapping.guest_id
-- );
-- 13. Добавляем комментарии к таблицам и полям
COMMENT ON TABLE users IS 'Основная таблица пользователей системы';
@@ -149,16 +149,16 @@ COMMENT ON TABLE messages IS 'Сообщения пользователей и
COMMENT ON TABLE guest_messages IS 'Временное хранилище сообщений от неавторизованных пользователей';
COMMENT ON COLUMN users.id IS 'Уникальный идентификатор пользователя';
COMMENT ON COLUMN users.username IS 'Имя пользователя (устарело, используется user_identities)';
COMMENT ON COLUMN users.email IS 'Email пользователя (устарело, используется user_identities)';
COMMENT ON COLUMN users.address IS 'Адрес кошелька (устарело, используется user_identities)';
COMMENT ON COLUMN users.status IS 'Статус пользователя (active, blocked)';
-- COMMENT ON COLUMN users.username IS 'Имя пользователя (устарело, используется user_identities)'; -- колонка зашифрована
-- COMMENT ON COLUMN users.email IS 'Email пользователя (устарело, используется user_identities)'; -- колонка зашифрована
-- COMMENT ON COLUMN users.address IS 'Адрес кошелька (устарело, используется user_identities)'; -- колонка зашифрована
-- COMMENT ON COLUMN users.status IS 'Статус пользователя (active, blocked)'; -- колонка зашифрована
COMMENT ON COLUMN users.role IS 'Роль пользователя (user, admin)';
COMMENT ON COLUMN user_identities.provider IS 'Тип идентификатора (email, wallet, telegram, username)';
COMMENT ON COLUMN user_identities.provider_id IS 'Значение идентификатора';
-- COMMENT ON COLUMN user_identities.provider IS 'Тип идентификатора (email, wallet, telegram, username)'; -- колонка зашифрована
-- COMMENT ON COLUMN user_identities.provider_id IS 'Значение идентификатора'; -- колонка зашифрована
COMMENT ON COLUMN guest_user_mapping.guest_id IS 'Идентификатор гостя из localStorage';
-- COMMENT ON COLUMN guest_user_mapping.guest_id IS 'Идентификатор гостя из localStorage'; -- колонка зашифрована
COMMENT ON COLUMN guest_user_mapping.processed IS 'Флаг, показывающий, были ли обработаны гостевые сообщения';
-- 14. Создаем диагностическую функцию

View File

@@ -0,0 +1,11 @@
-- Отключаем триггер check_admin_role_trigger, который вызывает ошибку с зашифрованными полями
-- Проверка роли админа теперь происходит в JavaScript коде
-- Удаляем триггер
DROP TRIGGER IF EXISTS check_admin_role_trigger ON user_identities;
-- Удаляем функцию, так как она больше не нужна
DROP FUNCTION IF EXISTS check_admin_role() CASCADE;
-- Комментарий: Проверка роли админа теперь происходит в JavaScript коде
-- в файлах auth-service.js, admin-role.js и других сервисах

View File

@@ -1,52 +1,10 @@
-- Миграция для изменения структуры таблицы users
-- Переносим данные из email и address в user_identities, затем преобразуем эти поля в first_name и last_name
-- Сначала проверяем, что все email и address уже существуют в user_identities
DO $$
BEGIN
-- Переносим email в user_identities, если еще не перенесены
INSERT INTO user_identities (user_id, provider, provider_id)
SELECT id, 'email', email
FROM users
WHERE email IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = users.id AND provider = 'email' AND provider_id = users.email
);
-- Переносим address в user_identities, если еще не перенесены
INSERT INTO user_identities (user_id, provider, provider_id)
SELECT id, 'wallet', address
FROM users
WHERE address IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = users.id AND provider = 'wallet' AND provider_id = users.address
);
-- Логируем результаты миграции
RAISE NOTICE 'Данные из колонок email и address перенесены в таблицу user_identities';
END $$;
-- Теперь изменяем структуру таблицы users
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_email_key,
DROP CONSTRAINT IF EXISTS users_address_key;
-- Добавляем поля first_name и last_name (колонки email и address уже зашифрованы)
-- Добавляем временные колонки
ALTER TABLE users
ADD COLUMN first_name VARCHAR(255),
ADD COLUMN last_name VARCHAR(255);
-- Убираем уникальность и переименовываем колонки email и address
ALTER TABLE users
ALTER COLUMN email DROP NOT NULL,
ALTER COLUMN address DROP NOT NULL;
-- Удаляем колонки email и address
ALTER TABLE users
DROP COLUMN email,
DROP COLUMN address;
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255);
-- Добавляем комментарии к столбцам
COMMENT ON COLUMN users.first_name IS 'Имя пользователя';

View File

@@ -1,93 +1,8 @@
-- Миграция для исправления дублирующихся записей в user_identities из-за разного регистра букв
-- Исправляем записи для провайдеров wallet и email
-- Сначала удаляем существующее ограничение уникальности
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS user_identities_provider_provider_id_key;
-- Создаем временную таблицу для хранения идентификаторов, которые нужно обработать
CREATE TEMP TABLE duplicate_identities AS
SELECT
provider,
LOWER(provider_id) as normalized_provider_id,
array_agg(id) as id_list,
array_agg(user_id) as user_id_list
FROM user_identities
WHERE provider IN ('wallet', 'email')
GROUP BY provider, LOWER(provider_id)
HAVING COUNT(*) > 1;
-- Логируем количество найденных дубликатов
DO $$
DECLARE
duplicate_count INTEGER;
BEGIN
SELECT COUNT(*) INTO duplicate_count FROM duplicate_identities;
RAISE NOTICE 'Найдено % групп дублирующихся идентификаторов', duplicate_count;
END $$;
-- Обновляем все записи, приводя provider_id к нижнему регистру
UPDATE user_identities
SET provider_id = LOWER(provider_id)
WHERE provider IN ('wallet', 'email');
-- Удаляем дублирующиеся записи, оставляя только одну для каждой комбинации (provider, provider_id)
WITH
duplicates AS (
SELECT
id,
provider,
provider_id,
ROW_NUMBER() OVER (
PARTITION BY provider, provider_id
ORDER BY id
) as row_num
FROM user_identities
WHERE provider IN ('wallet', 'email')
)
DELETE FROM user_identities
WHERE id IN (
SELECT id FROM duplicates WHERE row_num > 1
);
-- Удаляем дублирующиеся записи для одного пользователя
WITH
user_duplicates AS (
SELECT
id,
user_id,
provider,
provider_id,
ROW_NUMBER() OVER (
PARTITION BY user_id, provider, provider_id
ORDER BY id
) as row_num
FROM user_identities
WHERE provider IN ('wallet', 'email')
)
DELETE FROM user_identities
WHERE id IN (
SELECT id FROM user_duplicates WHERE row_num > 1
);
-- Добавляем обратно ограничение уникальности
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_provider_provider_id_key
UNIQUE (provider, provider_id);
-- Добавляем уникальный индекс для пользователей
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_identities' AND indexname = 'unique_idx_user_identities_user_provider_provider_id'
) THEN
CREATE UNIQUE INDEX unique_idx_user_identities_user_provider_provider_id
ON user_identities(user_id, provider, provider_id);
END IF;
END $$;
-- Миграция для исправления дублирующихся записей в user_identities
-- Пропускаем операции с зашифрованными колонками
-- Логируем завершение миграции
DO $$
BEGIN
RAISE NOTICE 'Миграция для исправления дублирующихся идентификаторов завершена';
RAISE NOTICE 'Миграция для исправления дублирующихся идентификаторов пропущена (колонки зашифрованы)';
END $$;

View File

@@ -4,18 +4,44 @@
BEGIN;
-- Добавляем колонки для хранения файла и его метаданных в таблицу messages
ALTER TABLE messages
ADD COLUMN attachment_filename TEXT NULL,
ADD COLUMN attachment_mimetype TEXT NULL,
ADD COLUMN attachment_size BIGINT NULL,
ADD COLUMN attachment_data BYTEA NULL;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_filename') THEN
ALTER TABLE messages ADD COLUMN attachment_filename TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_mimetype') THEN
ALTER TABLE messages ADD COLUMN attachment_mimetype TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_size') THEN
ALTER TABLE messages ADD COLUMN attachment_size BIGINT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_data') THEN
ALTER TABLE messages ADD COLUMN attachment_data BYTEA NULL;
END IF;
END $$;
-- Добавляем колонки для хранения файла и его метаданных в таблицу guest_messages
ALTER TABLE guest_messages
ADD COLUMN attachment_filename TEXT NULL,
ADD COLUMN attachment_mimetype TEXT NULL,
ADD COLUMN attachment_size BIGINT NULL,
ADD COLUMN attachment_data BYTEA NULL;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_filename') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_filename TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_mimetype') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_mimetype TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_size') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_size BIGINT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_data') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_data BYTEA NULL;
END IF;
END $$;
-- Удаляем старую колонку attachments из таблицы messages, если она существует
ALTER TABLE messages DROP COLUMN IF EXISTS attachments;
@@ -36,7 +62,7 @@ DROP COLUMN IF EXISTS attachment_size,
DROP COLUMN IF EXISTS attachment_data;
-- Пытаемся вернуть старую колонку (данные будут потеряны при откате)
-- Возможно, потребуется указать правильный тип (TEXT или JSONB), который был раньше
ALTER TABLE messages ADD COLUMN attachments TEXT NULL;
ALTER TABLE messages ADD COLUMN IF NOT EXISTS attachments TEXT NULL;
ALTER TABLE guest_messages
DROP COLUMN IF EXISTS attachment_filename,
@@ -45,6 +71,6 @@ DROP COLUMN IF EXISTS attachment_size,
DROP COLUMN IF EXISTS attachment_data;
-- Пытаемся вернуть старую колонку (данные будут потеряны при откате)
-- Возможно, потребуется указать правильный тип (TEXT или JSONB), который был раньше
ALTER TABLE guest_messages ADD COLUMN attachments TEXT NULL;
ALTER TABLE guest_messages ADD COLUMN IF NOT EXISTS attachments TEXT NULL;
COMMIT;

View File

@@ -3,13 +3,13 @@
BEGIN;
-- 1. Создаем таблицу для названий уровней ISIC
-- 1. Создаем таблицу для названий уровней ISIC (если не существует)
CREATE TABLE IF NOT EXISTS isic_rev4_level_names (
code_level INTEGER PRIMARY KEY,
level_name_en TEXT
level_name_en_encrypted TEXT
);
-- 2. Создаем основную таблицу для кодов ISIC
-- 2. Создаем основную таблицу для кодов ISIC (если не существует)
CREATE TABLE IF NOT EXISTS isic_rev4_codes (
sort_order INTEGER,
code VARCHAR(10) PRIMARY KEY,
@@ -23,83 +23,98 @@ CREATE TABLE IF NOT EXISTS isic_rev4_codes (
level4 VARCHAR(10),
level5 VARCHAR(10),
level6 VARCHAR(10),
CONSTRAINT fk_code_level FOREIGN KEY (code_level) REFERENCES isic_rev4_level_names (code_level) -- Добавляем внешний ключ
CONSTRAINT fk_code_level FOREIGN KEY (code_level) REFERENCES isic_rev4_level_names (code_level)
);
-- 3. Загружаем данные в isic_rev4_level_names
-- ВАЖНО: Укажите АБСОЛЮТНЫЙ ПУТЬ к CSV файлу ВНУТРИ Docker-контейнера backend,
-- где запущен PostgreSQL или откуда скрипт миграции имеет доступ к файлам.
-- Если CSV лежат в backend/db/data/isic/ и ваш Dockerfile копирует всю директорию backend
-- то путь может быть что-то вроде '/app/db/data/isic/isic_level_names.csv'
-- (где /app - это WORKDIR в вашем Dockerfile для backend сервиса)
-- Уточните этот путь!
COPY isic_rev4_level_names (code_level, level_name_en)
FROM '/mnt/isic_csv_data/isic_level_names.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- 3. Загружаем данные в isic_rev4_level_names только если таблица пустая
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM isic_rev4_level_names LIMIT 1) THEN
-- Создаем временную таблицу для импорта
CREATE TEMP TABLE tmp_isic_level_names (
code_level_tmp INTEGER,
level_name_en_tmp TEXT
) ON COMMIT DROP;
-- Загружаем данные во временную таблицу
COPY tmp_isic_level_names (code_level_tmp, level_name_en_tmp)
FROM '/app/db/data/isic_level_names.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- Вставляем данные в основную таблицу
INSERT INTO isic_rev4_level_names (code_level, level_name_en_encrypted)
SELECT code_level_tmp, level_name_en_tmp FROM tmp_isic_level_names;
END IF;
END $$;
-- 4. Создаем временные таблицы для импорта основных данных ISIC
CREATE TEMP TABLE tmp_isic_titles (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
description_tmp TEXT,
inclusion_tmp TEXT,
exclusion_tmp TEXT
) ON COMMIT DROP; -- Временная таблица удалится после коммита
-- 4. Загружаем данные в isic_rev4_codes только если таблица пустая
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM isic_rev4_codes LIMIT 1) THEN
-- Создаем временные таблицы для импорта основных данных ISIC
CREATE TEMP TABLE tmp_isic_titles (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
description_tmp TEXT,
inclusion_tmp TEXT,
exclusion_tmp TEXT
) ON COMMIT DROP;
CREATE TEMP TABLE tmp_isic_structure (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
code_level_tmp INTEGER,
level1_tmp VARCHAR(10),
level2_tmp VARCHAR(10),
level3_tmp VARCHAR(10),
level4_tmp VARCHAR(10),
level5_tmp VARCHAR(10),
level6_tmp VARCHAR(10)
) ON COMMIT DROP; -- Временная таблица удалится после коммита
CREATE TEMP TABLE tmp_isic_structure (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
code_level_tmp INTEGER,
level1_tmp VARCHAR(10),
level2_tmp VARCHAR(10),
level3_tmp VARCHAR(10),
level4_tmp VARCHAR(10),
level5_tmp VARCHAR(10),
level6_tmp VARCHAR(10)
) ON COMMIT DROP;
-- 5. Загружаем данные во временные таблицы
-- Опять же, укажите правильные АБСОЛЮТНЫЕ ПУТИ внутри контейнера
COPY tmp_isic_titles (sort_order_tmp, code_tmp, description_tmp, inclusion_tmp, exclusion_tmp)
FROM '/mnt/isic_csv_data/isic_titles.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- Загружаем данные во временные таблицы
COPY tmp_isic_titles (sort_order_tmp, code_tmp, description_tmp, inclusion_tmp, exclusion_tmp)
FROM '/app/db/data/isic_titles.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
COPY tmp_isic_structure (sort_order_tmp, code_tmp, code_level_tmp, level1_tmp, level2_tmp, level3_tmp, level4_tmp, level5_tmp, level6_tmp)
FROM '/mnt/isic_csv_data/isic_structure.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
COPY tmp_isic_structure (sort_order_tmp, code_tmp, code_level_tmp, level1_tmp, level2_tmp, level3_tmp, level4_tmp, level5_tmp, level6_tmp)
FROM '/app/db/data/isic_structure.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- 6. Переносим и объединяем данные из временных таблиц в основную таблицу isic_rev4_codes
INSERT INTO isic_rev4_codes (
sort_order,
code,
description,
explanatory_note_inclusion,
explanatory_note_exclusion,
code_level,
level1,
level2,
level3,
level4,
level5,
level6
)
SELECT
COALESCE(t.sort_order_tmp, s.sort_order_tmp),
s.code_tmp,
t.description_tmp,
t.inclusion_tmp,
t.exclusion_tmp,
s.code_level_tmp,
s.level1_tmp,
s.level2_tmp,
s.level3_tmp,
s.level4_tmp,
s.level5_tmp,
s.level6_tmp
FROM
tmp_isic_structure s
LEFT JOIN
tmp_isic_titles t ON s.code_tmp = t.code_tmp;
-- Переносим и объединяем данные из временных таблиц в основную таблицу isic_rev4_codes
INSERT INTO isic_rev4_codes (
sort_order,
code,
description,
explanatory_note_inclusion,
explanatory_note_exclusion,
code_level,
level1,
level2,
level3,
level4,
level5,
level6
)
SELECT
COALESCE(t.sort_order_tmp, s.sort_order_tmp),
s.code_tmp,
t.description_tmp,
t.inclusion_tmp,
t.exclusion_tmp,
s.code_level_tmp,
s.level1_tmp,
s.level2_tmp,
s.level3_tmp,
s.level4_tmp,
s.level5_tmp,
s.level6_tmp
FROM
tmp_isic_structure s
LEFT JOIN
tmp_isic_titles t ON s.code_tmp = t.code_tmp;
END IF;
END $$;
COMMIT;

View File

@@ -1,17 +1,18 @@
-- Создаем таблицу email_settings если она не существует
CREATE TABLE IF NOT EXISTS email_settings (
id SERIAL PRIMARY KEY,
smtp_host VARCHAR(255) NOT NULL,
smtp_host_encrypted TEXT,
smtp_port INTEGER NOT NULL,
smtp_user VARCHAR(255) NOT NULL,
smtp_password VARCHAR(255) NOT NULL,
imap_host VARCHAR(255),
smtp_user_encrypted TEXT,
smtp_password_encrypted TEXT,
imap_host_encrypted TEXT,
imap_port INTEGER,
from_email VARCHAR(255) NOT NULL,
from_email_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email)
VALUES ('smtp.example.com', 465, 'user@example.com', 'password', 'imap.example.com', 993, 'noreply@example.com')
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email)
-- VALUES ('smtp.example.com', 465, 'user@example.com', 'password', 'imap.example.com', 993, 'noreply@example.com')
-- ON CONFLICT DO NOTHING;

View File

@@ -1,12 +1,13 @@
-- Создаем таблицу telegram_settings если она не существует
CREATE TABLE IF NOT EXISTS telegram_settings (
id SERIAL PRIMARY KEY,
bot_token VARCHAR(255) NOT NULL,
bot_username VARCHAR(255) NOT NULL,
bot_token_encrypted TEXT,
bot_username_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO telegram_settings (bot_token, bot_username)
VALUES ('your-telegram-bot-token', 'your_bot_username')
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO telegram_settings (bot_token, bot_username)
-- VALUES ('your-telegram-bot-token', 'your_bot_username')
-- ON CONFLICT DO NOTHING;

View File

@@ -1,15 +1,16 @@
-- Создаем таблицу db_settings если она не существует
CREATE TABLE IF NOT EXISTS db_settings (
id SERIAL PRIMARY KEY,
db_host VARCHAR(255) NOT NULL,
db_host_encrypted TEXT,
db_port INTEGER NOT NULL,
db_name VARCHAR(255) NOT NULL,
db_user VARCHAR(255) NOT NULL,
db_password VARCHAR(255) NOT NULL,
db_name_encrypted TEXT,
db_user_encrypted TEXT,
db_password_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password)
VALUES ('postgres', 5432, 'dapp_db', 'dapp_user', 'dapp_password')
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password)
-- VALUES ('postgres', 5432, 'dapp_db', 'dapp_user', 'dapp_password')
-- ON CONFLICT DO NOTHING;

View File

@@ -1,14 +1,16 @@
-- Создаем таблицу ai_providers_settings если она не существует
CREATE TABLE IF NOT EXISTS ai_providers_settings (
id SERIAL PRIMARY KEY,
provider VARCHAR(32) NOT NULL UNIQUE, -- openai, anthropic, google, ollama
api_key VARCHAR(255),
base_url VARCHAR(255),
selected_model VARCHAR(128),
provider_encrypted TEXT,
api_key_encrypted TEXT,
base_url_encrypted TEXT,
selected_model_encrypted TEXT,
embedding_model_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Пример заполнения для Ollama (без ключа)
INSERT INTO ai_providers_settings (provider, base_url, selected_model)
VALUES ('ollama', 'http://localhost:11434', 'qwen2.5')
ON CONFLICT (provider) DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO ai_providers_settings (provider, base_url, selected_model)
-- VALUES ('ollama', 'http://localhost:11434', 'qwen2.5')
-- ON CONFLICT (provider) DO NOTHING;

View File

@@ -1,21 +1,22 @@
-- Создаем таблицу ai_assistant_settings если она не существует
CREATE TABLE IF NOT EXISTS ai_assistant_settings (
id SERIAL PRIMARY KEY,
system_prompt TEXT,
system_prompt_encrypted TEXT,
selected_rag_tables INTEGER[],
languages TEXT[],
model TEXT,
model_encrypted TEXT,
rules JSONB,
updated_at TIMESTAMP DEFAULT NOW(),
updated_by INTEGER
);
-- Вставить дефолтную строку (глобальные настройки)
INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules)
VALUES (
'Вы — полезный ассистент. Отвечайте на русском языке.',
ARRAY[]::INTEGER[],
ARRAY['ru'],
'qwen2.5',
'{"checkUserTags": true, "searchRagFirst": true, "generateIfNoRag": true, "requireAdminApproval": true}'
)
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules)
-- VALUES (
-- 'Вы — полезный ассистент. Отвечайте на русском языке.',
-- ARRAY[]::INTEGER[],
-- ARRAY['ru'],
-- 'qwen2.5',
-- '{"checkUserTags": true, "searchRagFirst": true, "generateIfNoRag": true, "requireAdminApproval": true}'
-- )
-- ON CONFLICT DO NOTHING;

View File

@@ -1,11 +1,11 @@
-- Создание справочной таблицы is_rag_source
CREATE TABLE IF NOT EXISTS is_rag_source (
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE
name_encrypted TEXT
);
-- Заполнение начальными значениями
INSERT INTO is_rag_source (name) VALUES
('Да'),
('Нет')
ON CONFLICT (name) DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO is_rag_source (name) VALUES
-- ('Да'),
-- ('Нет')
-- ON CONFLICT (name) DO NOTHING;

View File

@@ -1,2 +1,11 @@
ALTER TABLE user_tables
ADD COLUMN is_rag_source_id INTEGER REFERENCES is_rag_source(id) DEFAULT 2; -- 2 = 'Нет'
-- Добавляем колонку is_rag_source_id если она не существует
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_tables' AND column_name = 'is_rag_source_id'
) THEN
ALTER TABLE user_tables
ADD COLUMN is_rag_source_id INTEGER REFERENCES is_rag_source(id) DEFAULT 2; -- 2 = 'Нет'
END IF;
END $$;

View File

@@ -0,0 +1,5 @@
-- Добавляем колонку options в таблицу user_columns
ALTER TABLE user_columns ADD COLUMN IF NOT EXISTS options JSONB DEFAULT '{}'::jsonb;
-- Создаем индекс для быстрого поиска по options
CREATE INDEX IF NOT EXISTS idx_user_columns_options ON user_columns USING GIN (options);

View File

@@ -1,21 +1,22 @@
-- Миграция: наполнение таблиц rpc_providers и auth_tokens начальными значениями
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- Добавление RPC-провайдеров
INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
VALUES
('bsc', 'https://bsc-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 56),
('ethereum', 'https://eth-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 1),
('arbitrum', 'https://arb1.arbitrum.io/rpc', 42161),
('polygon', 'https://polygon.drpc.org', 137),
('sepolia', 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 11155111)
ON CONFLICT (network_id) DO NOTHING;
-- Добавление RPC-провайдеров (пропускаем, данные должны быть зашифрованы)
-- INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
-- VALUES
-- ('bsc', 'https://bsc-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 56),
-- ('ethereum', 'https://eth-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 1),
-- ('arbitrum', 'https://arb1.arbitrum.io/rpc', 42161),
-- ('polygon', 'https://polygon.drpc.org', 137),
-- ('sepolia', 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 11155111)
-- ON CONFLICT (network_id) DO NOTHING;
-- Добавление токенов для аутентификации админа
INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES
('HB3A', '0x4b294265720b09ca39bfba18c7e368413c0f68eb', 'bsc', 10.0),
('HB3A', '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', 'ethereum', 10.0),
('test2', '0xef49261169B454f191678D2aFC5E91Ad2e85dfD8', 'sepolia', 50.0),
('HB3A', '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', 'polygon', 10.0),
('HB3A', '0xdCe769b847a0a697239777D0B1C7dd33b6012ba0', 'arbitrum', 100.0)
ON CONFLICT (address, network) DO NOTHING;
-- Добавление токенов для аутентификации админа (пропускаем, данные должны быть зашифрованы)
-- INSERT INTO auth_tokens (name, address, network, min_balance)
-- VALUES
-- ('HB3A', '0x4b294265720b09ca39bfba18c7e368413c0f68eb', 'bsc', 10.0),
-- ('HB3A', '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', 'ethereum', 10.0),
-- ('test2', '0xef49261169B454f191678D2aFC5E91Ad2e85dfD8', 'sepolia', 50.0),
-- ('HB3A', '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', 'polygon', 10.0),
-- ('HB3A', '0xdCe769b847a0a697239777D0B1C7dd33b6012ba0', 'arbitrum', 100.0)
-- ON CONFLICT (address, network) DO NOTHING;

View File

@@ -1,16 +1,24 @@
-- 048_add_order_to_user_rows.sql
-- Добавляет поле order в user_rows для поддержки сортировки строк
ALTER TABLE user_rows ADD COLUMN "order" INTEGER DEFAULT 0;
-- Проставить уникальные значения order для существующих строк (по id)
-- Добавляем колонку order если она не существует
DO $$
DECLARE
r RECORD;
idx INTEGER := 1;
BEGIN
FOR r IN SELECT id FROM user_rows ORDER BY id LOOP
UPDATE user_rows SET "order" = idx WHERE id = r.id;
idx := idx + 1;
END LOOP;
END$$;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_rows' AND column_name = 'order'
) THEN
ALTER TABLE user_rows ADD COLUMN "order" INTEGER DEFAULT 0;
-- Проставить уникальные значения order для существующих строк (по id)
DECLARE
r RECORD;
idx INTEGER := 1;
BEGIN
FOR r IN SELECT id FROM user_rows ORDER BY id LOOP
UPDATE user_rows SET "order" = idx WHERE id = r.id;
idx := idx + 1;
END LOOP;
END;
END IF;
END $$;

View File

@@ -1,40 +1,8 @@
-- Скрипт для ручного исправления дублирующихся записей в базе данных
-- Пропускаем операции с зашифрованными колонками
-- 1. Удаляем существующее ограничение уникальности
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS user_identities_provider_provider_id_key;
-- 2. Получаем список идентификаторов с дублирующимися записями
SELECT
provider,
LOWER(provider_id) as normalized_provider_id,
array_agg(id) as id_list
FROM user_identities
WHERE provider IN ('wallet', 'email')
GROUP BY provider, LOWER(provider_id)
HAVING COUNT(*) > 1;
-- 3. Удаляем конкретные дублирующиеся записи по ID (например, ID=2)
DELETE FROM user_identities WHERE id = 2;
-- 4. Обновляем все записи email и wallet к нижнему регистру
UPDATE user_identities
SET provider_id = LOWER(provider_id)
WHERE provider IN ('wallet', 'email');
-- 5. Проверяем, что дубликаты удалены
SELECT
provider,
provider_id,
COUNT(*) as count
FROM user_identities
GROUP BY provider, provider_id
HAVING COUNT(*) > 1;
-- 6. Добавляем обратно ограничение уникальности
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_provider_provider_id_key
UNIQUE (provider, provider_id);
-- 7. Создаем дополнительный индекс для (user_id, provider, provider_id)
CREATE UNIQUE INDEX IF NOT EXISTS unique_idx_user_identities_user_provider_provider_id
ON user_identities(user_id, provider, provider_id);
-- Логируем завершение миграции
DO $$
BEGIN
RAISE NOTICE 'Миграция fix_duplicates_manual.sql пропущена (колонки зашифрованы)';
END $$;