Загрузка данных


-- ============================================================================
-- ПОЛНЫЙ СКРИПТ БАЗЫ ДАННЫХ «БИБЛИОТЕКА» (СУБД PostgreSQL)
-- ============================================================================

-- ============================================================================
-- УРОВЕНЬ «УДОВЛЕТВОРИТЕЛЬНО» (БАЗОВЫЙ)
-- ============================================================================

/*
1. ОПИСАНИЕ ER-ДИАГРАММЫ И СХЕМЫ БД (3НФ):
   - Сущность Books (Книги) [1] <---> [M] Loans (Выдачи) [M] <---> [1] Readers (Читатели).
   - Все таблицы находятся в Третьей нормальной форме (3НФ):
     * 1НФ: Все атрибуты атомарны, отсутствуют повторяющиеся группы.
     * 2НФ: Все неключевые атрибуты полностью зависят от первичного ключа.
     * 3НФ: Отсутствуют транзитивные зависимости между неключевыми атрибутами
            (данные о книгах и читателях вынесены в отдельные справочники и связаны
            через внешние ключи в таблице выдач Loans).
*/

-- 2. СОЗДАНИЕ ТАБЛИЦ С ОГРАНИЧЕНИЯМИ ЦЕЛОСТНОСТИ

DROP VIEW IF EXISTS v_readers_masked CASCADE;
DROP TABLE IF EXISTS Audit_Log CASCADE;
DROP TABLE IF EXISTS Loans CASCADE;
DROP TABLE IF EXISTS Books CASCADE;
DROP TABLE IF EXISTS Readers CASCADE;

-- Таблица книг (Books)
CREATE TABLE Books (
    "Код" SERIAL PRIMARY KEY,
    "Название" VARCHAR(255) NOT NULL,
    "Автор" VARCHAR(150) NOT NULL,
    "Год издания" INT NOT NULL CHECK ("Год издания" > 1900 AND "Год издания" <= EXTRACT(YEAR FROM CURRENT_DATE)),
    "Количество экземпляров" INT NOT NULL DEFAULT 1 CHECK ("Количество экземпляров" >= 0)
);

-- Таблица читателей (Readers)
CREATE TABLE Readers (
    "Код" SERIAL PRIMARY KEY,
    "ФИО" VARCHAR(150) NOT NULL,
    "Дата рождения" DATE NOT NULL CHECK ("Дата рождения" > '1900-01-01' AND "Дата рождения" < CURRENT_DATE),
    "Адрес" VARCHAR(255) NOT NULL,
    "Телефон" VARCHAR(20) NOT NULL UNIQUE
);

-- Таблица выдач (Loans)
CREATE TABLE Loans (
    "Код выдачи" SERIAL PRIMARY KEY,
    "Книга" INT NOT NULL REFERENCES Books("Код") ON UPDATE CASCADE ON DELETE RESTRICT,
    "Читатель" INT NOT NULL REFERENCES Readers("Код") ON UPDATE CASCADE ON DELETE RESTRICT,
    "Дата выдачи" DATE NOT NULL DEFAULT CURRENT_DATE,
    "Дата возврата" DATE NULL
);

-- 3. ДОБАВЛЕНИЕ 10 СТРОК ТЕСТОВЫХ ДАННЫХ В КАЖДУЮ ТАБЛИЦУ

INSERT INTO Books ("Название", "Автор", "Год издания", "Количество экземпляров") VALUES
('Мастер и Маргарита', 'Булгаков М.А.', 1967, 5),
('Собачье сердце', 'Булгаков М.А.', 1987, 3),
('Тихий Дон', 'Шолохов М.А.', 1928, 4),
('Доктор Живаго', 'Пастернак Б.Л.', 1957, 2),
('Архипелаг ГУЛАГ', 'Солженицын А.И.', 1973, 3),
('Двенадцать стульев', 'Ильф И.А., Петров Е.П.', 1928, 6),
('Золотой телёнок', 'Ильф И.А., Петров Е.П.', 1931, 4),
('Пикник на обочине', 'Стругацкие А. и Б.', 1972, 5),
('Трудно быть богом', 'Стругацкие А. и Б.', 1964, 0), -- 0 экземпляров для проверки отказа выдачи
('Сто лет одиночества', 'Маркес Г.Г.', 1967, 3);

INSERT INTO Readers ("ФИО", "Дата рождения", "Адрес", "Телефон") VALUES
('Иванов Иван Иванович', '1990-05-14', 'г. Москва, ул. Ленина, д. 10, кв. 5', '+79001112233'),
('Петрова Анна Сергеевна', '1985-11-23', 'г. Москва, пр. Мира, д. 45, кв. 12', '+79002223344'),
('Сидоров Алексей Дмитриевич', '1998-02-10', 'г. Москва, ул. Тверская, д. 8, кв. 30', '+79003334455'),
('Козлова Мария Владимировна', '2001-07-19', 'г. Москва, ул. Арбат, д. 15, кв. 4', '+79004445566'),
('Смирнов Дмитрий Олегович', '1993-09-01', 'г. Москва, ул. Садовая, д. 3, кв. 18', '+79005556677'),
('Морозова Елена Викторовна', '1988-12-12', 'г. Москва, ул. Гагарина, д. 22, кв. 7', '+79006667788'),
('Волков Сергей Николаевич', '1995-04-05', 'г. Москва, ул. Пушкина, д. 11, кв. 9', '+79007778899'),
('Лебедева Ольга Игоревна', '2003-08-27', 'г. Москва, ул. Чехова, д. 6, кв. 21', '+79008889900'),
('Соколов Павел Андреевич', '1992-01-30', 'г. Москва, ул. Кирова, д. 19, кв. 14', '+79009990011'),
('Новикова Татьяна Юрьевна', '1997-06-15', 'г. Москва, ул. Ломоносова, д. 2, кв. 3', '+79010001122');

INSERT INTO Loans ("Книга", "Читатель", "Дата выдачи", "Дата возврата") VALUES
(1, 1, '2026-09-01', '2026-09-15'),
(2, 2, '2026-09-05', '2026-09-20'),
(1, 3, '2026-09-10', NULL),
(6, 4, '2026-09-12', '2026-09-25'),
(7, 5, '2026-09-15', NULL),
(8, 6, '2026-09-18', '2026-10-02'),
(8, 7, '2026-09-20', NULL),
(3, 8, '2026-09-22', '2026-10-05'),
(4, 9, '2026-09-25', NULL),
(1, 10, '2026-10-01', NULL);

-- 4. ТРИ SELECT-ЗАПРОСА

-- Запрос 4.1: Список книг с количеством экземпляров
SELECT 
    "Код",
    "Название",
    "Автор",
    "Год издания",
    "Количество экземпляров"
FROM Books
ORDER BY "Название";

-- Запрос 4.2: Активные выдачи (где дата возврата NULL)
SELECT 
    l."Код выдачи",
    b."Название" AS "Книга",
    r."ФИО" AS "Читатель",
    l."Дата выдачи",
    l."Дата возврата"
FROM Loans l
JOIN Books b ON l."Книга" = b."Код"
JOIN Readers r ON l."Читатель" = r."Код"
WHERE l."Дата возврата" IS NULL
ORDER BY l."Дата выдачи";

-- Запрос 4.3: Топ-5 самых популярных авторов по количеству выданных книг
SELECT 
    b."Автор",
    COUNT(l."Код выдачи") AS "Количество выданных книг"
FROM Books b
JOIN Loans l ON b."Код" = l."Книга"
GROUP BY b."Автор"
ORDER BY "Количество выданных книг" DESC
LIMIT 5;

-- 5. МЕРЫ ЗАЩИТЫ НА УРОВНЕ БД (БАЗОВЫЙ УРОВЕНЬ)
/*
   Описание мер защиты базового уровня:
   1. Ограничение целостности данных (NOT NULL, CHECK, UNIQUE, FOREIGN KEY с запретом
      каскадного удаления ON DELETE RESTRICT, чтобы нельзя было удалить книгу с историей выдач).
   2. Создание отдельного базового пользователя БД с минимально необходимыми привилегиями
      на конкретные таблицы вместо использования суперпользователя postgres.
*/
DO $$
BEGIN
    IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'lib_basic_user') THEN
        CREATE USER lib_basic_user WITH PASSWORD 'BasicSecurePassword123!';
    END IF;
END
$$;

GRANT SELECT ON Books, Readers TO lib_basic_user;
GRANT SELECT, INSERT, UPDATE ON Loans TO lib_basic_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO lib_basic_user;


-- ============================================================================
-- УРОВЕНЬ «ХОРОШО» (ПРОДВИНУТЫЙ)
-- ============================================================================

-- 1. ХРАНИМАЯ ПРОЦЕДУРА (ФУНКЦИЯ) ВЫДАЧИ КНИГИ sp_issue_book(book_id, reader_id)

CREATE OR REPLACE FUNCTION sp_issue_book(p_book_id INT, p_reader_id INT)
RETURNS TABLE (
    "Код результата" INT,
    "Статус" VARCHAR(20),
    "Сообщение" TEXT
) 
LANGUAGE plpgsql
AS $$
DECLARE
    v_available_copies INT;
    v_reader_exists BOOLEAN;
    v_already_issued BOOLEAN;
BEGIN
    -- Проверка существования читателя
    SELECT EXISTS(SELECT 1 FROM Readers WHERE "Код" = p_reader_id) INTO v_reader_exists;
    IF NOT v_reader_exists THEN
        RETURN QUERY SELECT 404, 'ОШИБКА'::VARCHAR(20), 'Читатель с указанным кодом не найден.'::TEXT;
        RETURN;
    END IF;

    -- Проверка существования книги и блокировка строки для избежания гонки данных
    SELECT "Количество экземпляров" INTO v_available_copies
    FROM Books
    WHERE "Код" = p_book_id
    FOR UPDATE;

    IF NOT FOUND THEN
        RETURN QUERY SELECT 404, 'ОШИБКА'::VARCHAR(20), 'Книга с указанным кодом не найдена.'::TEXT;
        RETURN;
    END IF;

    -- Проверка, не находится ли уже эта же книга на руках у данного читателя
    SELECT EXISTS(
        SELECT 1 FROM Loans 
        WHERE "Книга" = p_book_id 
          AND "Читатель" = p_reader_id 
          AND "Дата возврата" IS NULL
    ) INTO v_already_issued;

    IF v_already_issued THEN
        RETURN QUERY SELECT 409, 'ОШИБКА'::VARCHAR(20), 'Эта книга уже выдана данному читателю и ещё не возвращена.'::TEXT;
        RETURN;
    END IF;

    -- Проверка наличия свободных экземпляров
    IF v_available_copies <= 0 THEN
        RETURN QUERY SELECT 400, 'ОШИБКА'::VARCHAR(20), 'Нет свободных экземпляров книги для выдачи.'::TEXT;
        RETURN;
    END IF;

    -- Создание записи о выдаче в таблице Loans
    INSERT INTO Loans ("Книга", "Читатель", "Дата выдачи", "Дата возврата")
    VALUES (p_book_id, p_reader_id, CURRENT_DATE, NULL);

    -- Уменьшение количества доступных экземпляров в таблице Books
    UPDATE Books
    SET "Количество экземпляров" = "Количество экземпляров" - 1
    WHERE "Код" = p_book_id;

    RETURN QUERY SELECT 200, 'УСПЕХ'::VARCHAR(20), 'Книга успешно выдана читателю.'::TEXT;

EXCEPTION
    WHEN OTHERS THEN
        RETURN QUERY SELECT 500, 'ОШИБКА'::VARCHAR(20), ('Внутренняя ошибка БД: ' || SQLERRM)::TEXT;
END;
$$;

-- 2. ТРИГГЕР НА ТАБЛИЦУ Loans ДЛЯ ПРОВЕРКИ ДАТ ВЫДАЧИ И ВОЗВРАТА

CREATE OR REPLACE FUNCTION fn_check_loan_dates()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        IF NEW."Дата возврата" IS NOT NULL AND NEW."Дата возврата" <= NEW."Дата выдачи" THEN
            RAISE EXCEPTION 'Ошибка при вставке: Дата возврата (%) должна быть больше даты выдачи (%).', 
                NEW."Дата возврата", NEW."Дата выдачи";
        END IF;
    ELSIF TG_OP = 'UPDATE' THEN
        IF NEW."Дата возврата" IS NOT NULL AND NEW."Дата возврата" < NEW."Дата выдачи" THEN
            RAISE EXCEPTION 'Ошибка при обновлении: Дата возврата (%) не может быть раньше даты выдачи (%).', 
                NEW."Дата возврата", NEW."Дата выдачи";
        END IF;
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_check_loan_dates
BEFORE INSERT OR UPDATE ON Loans
FOR EACH ROW
EXECUTE FUNCTION fn_check_loan_dates();

-- 3. ИНДЕКСЫ И ОБОСНОВАНИЕ

CREATE INDEX idx_readers_fio ON Readers USING btree ("ФИО");
CREATE INDEX idx_books_title ON Books USING btree ("Название");
CREATE INDEX idx_loans_dates ON Loans USING btree ("Дата выдачи", "Дата возврата");

/*
   Краткое обоснование выбора полей и типа индекса (B-Tree):
   1. Поля "ФИО" (Readers) и "Название" (Books) регулярно используются при поиске
      карточки читателя и книги, а также при сортировке (ORDER BY).
   2. Поля "Дата выдачи" и "Дата возврата" (Loans) постоянно участвуют в фильтрации
      активных выдач (WHERE "Дата возврата" IS NULL) и отчётах за период.
   3. Тип индекса B-Tree (сбалансированное дерево) выбран потому, что он эффективно
      поддерживает как точное совпадение (=), так и диапазонные запросы (<, >, BETWEEN),
      префиксный поиск строк, сортировку (ORDER BY) и проверку на IS NULL за время O(log N).
*/

-- 4. РАЗГРАНИЧЕНИЕ ПРАВ (РОЛИ lib_admin И lib_librarian)

DO $$
BEGIN
    IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'lib_admin') THEN
        CREATE ROLE lib_admin WITH LOGIN PASSWORD 'AdminStrongPassword123!';
    END IF;
    IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'lib_librarian') THEN
        CREATE ROLE lib_librarian WITH LOGIN PASSWORD 'LibrarianStrongPassword123!';
    END IF;
END
$$;

-- Полный доступ для администратора (lib_admin)
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO lib_admin;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO lib_admin;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO lib_admin;

-- Ограниченный доступ для библиотекаря (lib_librarian): просмотр отчётов и выдача книг
GRANT SELECT ON Books, Readers, Loans TO lib_librarian;
GRANT INSERT, UPDATE ("Дата возврата") ON Loans TO lib_librarian;
GRANT UPDATE ("Количество экземпляров") ON Books TO lib_librarian;
GRANT USAGE, SELECT ON SEQUENCE loans_Код\ выдачи_seq TO lib_librarian;
GRANT EXECUTE ON FUNCTION sp_issue_book(INT, INT) TO lib_librarian;

-- Явный запрет лишних операций для библиотекаря
REVOKE DELETE ON Books, Readers, Loans FROM lib_librarian;
REVOKE INSERT, UPDATE ON Readers FROM lib_librarian;


-- ============================================================================
-- УРОВЕНЬ «ОТЛИЧНО» (ВЫСОКИЙ)
-- ============================================================================

-- 1. АУДИТ ДОСТУПА (ТАБЛИЦА Audit_Log И ТРИГГЕР НА ТАБЛИЦУ Books)

CREATE TABLE Audit_Log (
    "Код" SERIAL PRIMARY KEY,
    "Дата" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "Пользователь" VARCHAR(100) NOT NULL,
    "Операция" VARCHAR(20) NOT NULL,
    "Таблица" VARCHAR(50) NOT NULL,
    "Первичный ключ записи" INT NOT NULL,
    "Старое значение" TEXT NULL,
    "Новое значение" TEXT NULL
);

CREATE OR REPLACE FUNCTION fn_audit_books_copies()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
    IF OLD."Количество экземпляров" IS DISTINCT FROM NEW."Количество экземпляров" THEN
        INSERT INTO Audit_Log (
            "Дата",
            "Пользователь",
            "Операция",
            "Таблица",
            "Первичный ключ записи",
            "Старое значение",
            "Новое значение"
        )
        VALUES (
            CURRENT_TIMESTAMP,
            CURRENT_USER,
            TG_OP,
            TG_TABLE_NAME,
            NEW."Код",
            'Количество экземпляров: ' || OLD."Количество экземпляров"::TEXT,
            'Количество экземпляров: ' || NEW."Количество экземпляров"::TEXT
        );
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_audit_books_copies
AFTER UPDATE OF "Количество экземпляров" ON Books
FOR EACH ROW
EXECUTE FUNCTION fn_audit_books_copies();

-- Разрешаем библиотекарю запись в таблицу аудита при срабатывании триггера
GRANT INSERT ON Audit_Log TO lib_librarian;
GRANT USAGE, SELECT ON SEQUENCE audit_log_Код_seq TO lib_librarian;

-- 2. ЗАЩИТА ЧУВСТВИТЕЛЬНЫХ ДАННЫХ (МАСКИРОВАНИЕ ЧЕРЕЗ VIEW)

/*
   Меры защиты персональных данных (ФИО, телефон, адрес):
   1. Динамическое маскирование данных через представления (VIEW): скрытие полного номера
      телефона в формате +7 (XXX) XXX-XX-XX для рядовых сотрудников и отчётов.
   2. Разграничение прав доступа на уровне объектов (RBAC): отзыв прямого доступа (REVOKE SELECT)
      к таблице Readers у роли lib_librarian и предоставление доступа только к маскированному VIEW.
   3. Шифрование на уровне приложения/БД (pgcrypto) и аудит доступа к таблицам.
*/

CREATE OR REPLACE VIEW v_readers_masked AS
SELECT 
    "Код",
    "ФИО",
    "Дата рождения",
    "Адрес",
    '+7 (XXX) XXX-' || RIGHT(REGEXP_REPLACE("Телефон", '[^0-9]', '', 'g'), 4) AS "Телефон"
FROM Readers;

-- Отзываем прямой доступ к таблице Readers у библиотекаря и выдаём права только на VIEW
REVOKE SELECT ON Readers FROM lib_librarian;
GRANT SELECT ON v_readers_masked TO lib_librarian;

-- Проверка работы маскированного представления
SELECT * FROM v_readers_masked;


-- ============================================================================
-- ТЕСТИРОВАНИЕ (УРОВНИ «ХОРОШО» И «ОТЛИЧНО»)
-- ============================================================================

-- Тест 1: Успешная выдача книги (Книга 2 -> Читатель 1) + запись в Audit_Log
SELECT * FROM sp_issue_book(2, 1);

-- Тест 2: Попытка выдать книгу без свободных экземпляров (Книга 9 имеет 0 экз.)
SELECT * FROM sp_issue_book(9, 1);

-- Тест 3: Попытка повторно выдать ту же книгу тому же читателю (Книга 2 -> Читатель 1)
SELECT * FROM sp_issue_book(2, 1);

-- Тест 4: Проверка таблицы аудита после выдачи книги через процедуру
SELECT * FROM Audit_Log;

-- Тест 5: Демонстрация срабатывания триггера при попытке указать некорректную дату возврата
-- (Перехвачено в блоке DO, чтобы скрипт не прерывал выполнение, а вывел сообщение триггера в консоль)
DO $$
BEGIN
    INSERT INTO Loans ("Книга", "Читатель", "Дата выдачи", "Дата возврата")
    VALUES (3, 1, '2026-10-20', '2026-10-10');
EXCEPTION
    WHEN OTHERS THEN
        RAISE NOTICE 'Тест триггера успешно перехватил ошибку: %', SQLERRM;
END;
$$;


-- ============================================================================
-- 3 & 4. ПЛАН РЕЗЕРВНОГО КОПИРОВАНИЯ И АНАЛИЗ РИСКОВ (УРОВЕНЬ «ОТЛИЧНО»)
-- ============================================================================

/*
3. РЕЗЕРВНОЕ КОПИРОВАНИЕ И ВОССТАНОВЛЕНИЕ:
   - План резервного копирования:
     * Полный бэкап (Full Backup): еженедельно (каждое воскресенье в 02:00 ночи).
     * Инкрементальный бэкап / архивация WAL-журналов (Continuous Archiving): непрерывно
       для возможности восстановления на произвольный момент времени (PITR).
     * Ежедневный логический дамп схемы и данных (pg_dump): каждый день в 03:00 ночи.
     * Хранение (правило 3-2-1): 3 копии данных на 2 разных носителях (локальный NAS + 
       защищённое облачное хранилище S3 с шифрованием), 1 копия на удалённой площадке.
   
   - Команда создания полного сжатого бэкапа в PostgreSQL (выполняется в терминале ОС):
     pg_dump -U postgres -h localhost -p 5432 -F c -b -v -f "/backups/library_full_20261028.backup" library_db
     (Для MS SQL Server аналог: BACKUP DATABASE LibraryDB TO DISK = 'C:\Backups\LibraryDB_Full.bak' WITH COMPRESSION, INIT;)

   - Процедура восстановления из бэкапа:
     1. Остановить доступ клиентских приложений к БД.
     2. Создать чистую БД (при полном крахе) и выполнить команду:
        pg_restore -U postgres -h localhost -d library_db -v "/backups/library_full_20261028.backup"
     3. Проверить целостность таблиц, ключей и работу триггеров.
     4. Возможные риски при восстановлении:
        * Потеря транзакций (RPO > 0), совершённых между моментом создания последнего дампа и сбоем
          (минимизируется включением WAL-архивации).
        * Несоответствие схемы БД версии прикладного ПО, если после бэкапа накатывались миграции.

4. АНАЛИЗ РИСКОВ И РЕКОМЕНДАЦИИ:
   Риск 1: SQL-инъекции (SQL Injection).
     - Меры защиты: использование параметризованных запросов и хранимых процедур/функций
       вместо динамической склейки SQL-строк на уровне БД и приложения.
     - Статус: РЕАЛИЗОВАНО (создана параметризованная функция sp_issue_book).

   Риск 2: Утечка персональных данных читателей (ФИО, телефон, адрес).
     - Меры защиты: маскирование данных через представления (VIEW) и отзыв прямых прав
       SELECT на таблицу Readers у рядовых ролей; шифрование трафика (SSL/TLS).
     - Статус: РЕАЛИЗОВАНО (создано представление v_readers_masked и настроены GRANT/REVOKE).

   Риск 3: Несанкционированное изменение количества книг или удаление истории выдач.
     - Меры защиты: ролевая модель доступа (RBAC), ограничение внешних ключей ON DELETE RESTRICT,
       триггерный аудит изменений критичных полей в таблицу Audit_Log.
     - Статус: РЕАЛИЗОВАНО (роли lib_admin/lib_librarian, триггер trg_audit_books_copies, таблица Audit_Log).

   Риск 4: Нарушение логической целостности данных (некорректные даты, отрицательный остаток книг).
     - Меры защиты: ограничения CHECK на уровне таблиц и триггеры валидации дат перед вставкой/обновлением.
     - Статус: РЕАЛИЗОВАНО (CHECK "Количество экземпляров" >= 0, триггер trg_check_loan_dates).

   Риск 5: Потеря данных из-за аппаратного сбоя сервера.
     - Меры защиты: регулярное резервное копирование (pg_dump + WAL) и репликация БД.
     - Статус: СПРОЕКТИРОВАНО (разработан регламент и команды резервного копирования).
*/


-- ============================================================================
-- 5. ДОКУМЕНТАЦИЯ И КОММЕНТАРИИ К ОБЪЕКТАМ БД (УРОВЕНЬ «ОТЛИЧНО»)
-- ============================================================================

COMMENT ON TABLE Books IS 'Справочник книг библиотечного фонда';
COMMENT ON COLUMN Books."Код" IS 'Уникальный идентификатор книги (Первичный ключ)';
COMMENT ON COLUMN Books."Название" IS 'Полное название книги';
COMMENT ON COLUMN Books."Автор" IS 'ФИО автора или список авторов книги';
COMMENT ON COLUMN Books."Год издания" IS 'Год публикации книги (строго больше 1900)';
COMMENT ON COLUMN Books."Количество экземпляров" IS 'Текущий остаток доступных экземпляров в библиотеке';

COMMENT ON TABLE Readers IS 'Справочник зарегистрированных читателей библиотеки';
COMMENT ON COLUMN Readers."Код" IS 'Уникальный идентификатор читателя (Первичный ключ)';
COMMENT ON COLUMN Readers."ФИО" IS 'Фамилия, имя и отчество читателя';
COMMENT ON COLUMN Readers."Дата рождения" IS 'Дата рождения читателя';
COMMENT ON COLUMN Readers."Адрес" IS 'Адрес фактического проживания читателя';
COMMENT ON COLUMN Readers."Телефон" IS 'Контактный номер телефона читателя (уникальный)';

COMMENT ON TABLE Loans IS 'Журнал выдачи и возврата книг читателями';
COMMENT ON COLUMN Loans."Код выдачи" IS 'Уникальный идентификатор операции выдачи (Первичный ключ)';
COMMENT ON COLUMN Loans."Книга" IS 'Внешний ключ на таблицу Books';
COMMENT ON COLUMN Loans."Читатель" IS 'Внешний ключ на таблицу Readers';
COMMENT ON COLUMN Loans."Дата выдачи" IS 'Дата фактической выдачи книги на руки';
COMMENT ON COLUMN Loans."Дата возврата" IS 'Дата фактического возврата книги (NULL, если книга ещё на руках)';

COMMENT ON TABLE Audit_Log IS 'Журнал аудита изменений количества экземпляров книг';
COMMENT ON COLUMN Audit_Log."Код" IS 'Уникальный идентификатор записи аудита';
COMMENT ON COLUMN Audit_Log."Дата" IS 'Точное время события изменения';
COMMENT ON COLUMN Audit_Log."Пользователь" IS 'Имя пользователя СУБД, выполнившего операцию';
COMMENT ON COLUMN Audit_Log."Операция" IS 'Тип SQL-операции (UPDATE)';
COMMENT ON COLUMN Audit_Log."Таблица" IS 'Имя целевой таблицы, в которой произошло изменение';
COMMENT ON COLUMN Audit_Log."Первичный ключ записи" IS 'Код изменённой записи в таблице Books';
COMMENT ON COLUMN Audit_Log."Старое значение" IS 'Значение поля до выполнения операции';
COMMENT ON COLUMN Audit_Log."Новое значение" IS 'Значение поля после выполнения операции';

COMMENT ON VIEW v_readers_masked IS 'Представление читателей с маскированием телефонных номеров для защиты ПДн';
COMMENT ON FUNCTION sp_issue_book(INT, INT) IS 'Хранимая функция оформления выдачи книги с проверкой остатков и защитой от дублей';
COMMENT ON FUNCTION fn_check_loan_dates() IS 'Триггерная функция контроля корректности дат выдачи и возврата книги';
COMMENT ON FUNCTION fn_audit_books_copies() IS 'Триггерная функция записи изменений количества экземпляров книг в журнал Audit_Log';