conspects.iliay.ar
CREATE DATABASE hw;
CREATE EXTENSION pgcrypto;

Statements

В базе данных Airline информация о рейсах самолётов задана в виде таблиц

Flights(
    FlightId integer, 
    FlightTime timestamp, 
    PlaneId integer,
    -- Дополнительные столбцы, при необходимости
)
Seats(
    PlaneId integer,
    SeatNo varchar(4), -- 123A
    -- Дополнительные столбцы, при необходимости
)

Реализуйте запросы к базе данных Airline с применением представлений, хранимых процедур и функций. При необходимости, вы можете создать дополнительные таблицы, представления и хранимые процедуры.

Возможность бронирования должна автоматически отключаться за трое суток до начала рейса. Продажа мест должна автоматически отключаться за три часа до начала рейса. Также должна быть предусмотрена возможность отключения бронирования и продаж вручную.

  1. FreeSeats(FlightId) — список мест, доступных для продажи и для бронирования.
  2. Reserve(UserId, Pass, FlightId, SeatNo) — пытается забронировать место на трое суток начиная с момента бронирования. Возвращает истину, если удалось и ложь — в противном случае.
  3. ExtendReservation(UserId, Pass, FlightId, SeatNo) — пытается продлить бронь места на трое суток начиная с момента продления. Возвращает истину, если удалось и ложь — в противном случае.
  4. BuyFree(FlightId, SeatNo) — пытается купить свободное место. Возвращает истину, если удалось и ложь — в противном случае.
  5. BuyReserved(UserId, Pass, FlightId, SeatNo) — пытается выкупить забронированное место (пользователи должны совпадать). Возвращает истину, если удалось и ложь — в противном случае.
  6. FlightsStatistics(UserId, Pass) — статистика по рейсам: возможность бронирования и покупки, число свободных, забронированных и проданных мест.
  7. FlightStat(UserId, Pass, FlightId) — статистика по рейсу: возможность бронирования и покупки, число свободных, забронированных и проданных мест.
  8. CompressSeats(FlightId) — оптимизирует занятость мест в самолете. В результате оптимизации, в начале самолета должны быть купленные места, затем — забронированные, а в конце — свободные. Примечание: клиенты, которые уже выкупили билеты, также должны быть пересажены.

0.1

PostgreSQL 14.5

0.2

DROP TABLE IF EXISTS Flights CASCADE;
DROP TABLE IF EXISTS Seats CASCADE;
DROP TABLE IF EXISTS Tickets CASCADE;
DROP TABLE IF EXISTS Users CASCADE;

-- Список рейсов
CREATE TABLE Flights (
       FlightId int GENERATED ALWAYS AS IDENTITY not null,
       FlightTime TIMESTAMP not null,
       PlaneId int not null,

       PRIMARY KEY (FlightId)
);

-- Список мест
CREATE TABLE Seats (
       PlaneId int not null,
       SeatNo VARCHAR(4) not null,

       PRIMARY KEY (PlaneId, SeatNo)
);

-- Описание пользователей: Идентификатор и захещированный пароль
CREATE TABLE Users (
       UserId int GENERATED ALWAYS AS IDENTITY not null,
       Pass TEXT not null,

       PRIMARY KEY (UserId)
);

-- Купленные и забронированные места. Если место забронированно то
-- BookedUntil указано время когда бронь слетает, а также указан
-- пользователь взявший бронь. Если билет на место куплен то Taken =
-- TRUE.
CREATE TABLE Tickets (
       SeatNo VARCHAR(4) not null,
       PlaneId int not null,
       FlightId int not null,
       BookedUntil TIMESTAMP,
       Taken bool not null,
       UserId int,

       PRIMARY KEY (SeatNo, PlaneId, FlightId) DEFERRABLE,

       FOREIGN KEY (FlightId)
       REFERENCES Flights(FlightId),

       FOREIGN KEY (PlaneId, SeatNo)
       REFERENCES Seats(PlaneId, SeatNo),

       FOREIGN KEY (UserId)
       REFERENCES Users(UserId)
);
-- Все места актуальных рейсов
CREATE OR REPLACE VIEW AllSeats AS
SELECT Flights.FlightId, Seats.PlaneId, Seats.SeatNo, Tickets.Taken, Tickets.BookedUntil, Tickets.UserId
FROM Flights
     NATURAL JOIN Seats
     NATURAL LEFT JOIN Tickets
WHERE Flights.FlightTime > NOW();

-- Все не купленные места актуальных рейсов
CREATE OR REPLACE VIEW FreeOrReservedTickets AS
SELECT FlightId, PlaneId, SeatNo, BookedUntil, UserId
FROM AllSeats
WHERE AllSeats.Taken IS NULL
      OR NOT AllSeats.Taken;


-- Все места актуальных рейсов
CREATE OR REPLACE VIEW FreeSeatsView AS
SELECT FlightId, PlaneId, SeatNo, BookedUntil, UserId
FROM FreeOrReservedTickets
WHERE BookedUntil IS NULL
      OR BookedUntil < NOW();

Fill

INSERT INTO Flights(FlightTime, PlaneId) VALUES
       ('2022-11-26 18:00:00', 0),
       ('2022-12-03 18:00:00', 0),
       ('2022-11-24 10:00:00', 1),
       ('2022-11-26 10:00:00', 1),
       ('2022-11-28 10:00:00', 2);

INSERT INTO Seats(PlaneId, SeatNo) VALUES
       (0, '001A'),
       (0, '001B'),
       (0, '002A'),
       (0, '002B'),
       (0, '003A'),
       (0, '003B'),
       (1, '001A'),
       (1, '001B'),
       (1, '001C'),
       (1, '002A'),
       (1, '002B'),
       (1, '002C'),
       (2, '0001'),
       (2, '0002'),
       (2, '0003'),
       (2, '0004');

INSERT INTO Users(Pass) VALUES
       (CRYPT('qwerty', GEN_SALT('bf'))),
       (CRYPT('123456', GEN_SALT('bf'))),
       (CRYPT('amogus', GEN_SALT('bf')));

INSERT INTO Tickets(SeatNo, PlaneId, FlightId, BookedUntil, Taken, UserId) VALUES
       ('001A', 0, 1, NULL, TRUE, 1),
       ('001B', 0, 1, '2022-11-28 17:00:00', FALSE, 2),
       ('001A', 0, 2, '2022-12-03 17:00:00', FALSE, 3),
       ('002A', 0, 2, NULL, TRUE, NULL),
       ('001B', 0, 2, NULL, FALSE, 2);

View

SELECT * FROM Flights;
SELECT * FROM Users;
SELECT * FROM Tickets;

0.3

1

CREATE OR REPLACE FUNCTION FreeSeats(_FlightId int)
RETURNS TABLE (
    SeatNo VARCHAR(4)
)
LANGUAGE plpgsql
AS $$
BEGIN
   RETURN QUERY (
         SELECT FreeSeatsView.SeatNo
      FROM FreeSeatsView
      WHERE FlightId = _FlightId
   );
END;
$$
SELECT * FROM FreeSeats(2);

2

-- Проверка что пользователь сущесвует и пароль корректный
CREATE OR REPLACE FUNCTION Authenticate(UserId INT, Pass TEXT)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN EXISTS (
           SELECT *
           FROM Users
           WHERE Users.UserId = Authenticate.UserId
                    AND Users.Pass = CRYPT(Authenticate.Pass, Users.Pass)
    );
END;
$$;

-- Проверка что место не забронированно и не куплено
CREATE OR REPLACE FUNCTION IsFreeSeat(_FlightId INT, _SeatNo VARCHAR(40))
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
BEGIN
    RETURN _SeatNo IN (
           SELECT FreeSeatsView.SeatNo
           FROM FreeSeatsView
           WHERE FlightId = _FlightId
    );
END;
$$;
SELECT * FROM Authenticate(1, 'qwerty');
SELECT * FROM IsFreeSeat(2, '002A');
CREATE OR REPLACE FUNCTION Reserve(_UserId INT, _Pass TEXT, _FlightId INT, _SeatNo VARCHAR(4))
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
DECLARE _PlaneId INT = (
    SELECT PlaneId
    FROM Flights
    WHERE FlightId = _FlightId
);
DECLARE _FlightTime TIMESTAMP = (
    SELECT FlightTime
    FROM Flights
    WHERE FlightId = _FlightId
);
BEGIN
    IF _PlaneId IS NULL OR _FlightTime IS NULL THEN
       RETURN FALSE;
    END IF;

    IF NOT Authenticate(_UserId, _Pass) THEN
       RETURN FALSE;
    END IF;

    IF _FlightTime < NOW() THEN
       RETURN FALSE;
    END IF;

    IF NOT IsFreeSeat(_FlightId, _SeatNo) THEN
       RETURN FALSE;
    END IF;

    INSERT INTO Tickets(SeatNo, PlaneId, FlightId, BookedUntil, Taken, UserId) VALUES
           (_SeatNo, _PlaneId, _FlightId, NOW() + INTERVAL '3' DAY, FALSE, _UserId)
    ON CONFLICT(SeatNo, PlaneId, FlightId) DO UPDATE
       SET UserId = _UserId,
           BookedUntil = NOW() + INTERVAL '3' DAY,
           Taken = FALSE;

    RETURN TRUE;
END;
$$
SELECT * FROM Reserve(3, 'amogus', 5, '0003');

3

CREATE OR REPLACE FUNCTION ExtendReservation(_UserId INT, _Pass TEXT, _FlightId INT, _SeatNo VARCHAR(4))
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
DECLARE _PlaneId INT = (
    SELECT PlaneId
    FROM Flights
    WHERE FlightId = _FlightId
);
DECLARE _FlightTime TIMESTAMP = (
    SELECT FlightTime
    FROM Flights
    WHERE FlightId = _FlightId
);
DECLARE _ReservedByUser INT = (
    SELECT UserId
    FROM Tickets
    WHERE PlaneId = _PlaneId
          AND FlightId = _FlightId
          AND SeatNo = _SeatNo
);
BEGIN
    IF _PlaneId IS NULL OR _FlightTime IS NULL OR _ReservedByUser IS NULL THEN
       RETURN FALSE;
    END IF;

    IF NOT Authenticate(_UserId, _Pass) THEN
       RETURN FALSE;
    END IF;

    IF _UserId != _ReservedByUser THEN
       RETURN FALSE;
    END IF;

    IF _FlightTime < NOW() THEN
       RETURN FALSE;
    END IF;

    UPDATE Tickets
    SET BookedUntil = NOW() + INTERVAL '3' DAY
    WHERE PlaneId = _PlaneId
          AND FlightId = _FlightId
          AND SeatNo = _SeatNo;

    RETURN TRUE;
END;
$$
SELECT * FROM ExtendReservation(1, 'qwerty', 2, '001B');

4

CREATE OR REPLACE FUNCTION BuyFree(_FlightId INT, _SeatNo VARCHAR(4))
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
DECLARE _PlaneId INT = (
    SELECT PlaneId
    FROM Flights
    WHERE FlightId = _FlightId
);
DECLARE _FlightTime TIMESTAMP = (
    SELECT FlightTime
    FROM Flights
    WHERE FlightId = _FlightId
);
BEGIN
    IF _PlaneId IS NULL OR _FlightTime IS NULL THEN
       RETURN FALSE;
    END IF;

    IF _FlightTime < NOW() THEN
       RETURN FALSE;
    END IF;

    IF NOT IsFreeSeat(_FlightId, _SeatNo) THEN
       RETURN FALSE;
    END IF;

    INSERT INTO Tickets(SeatNo, PlaneId, FlightId, BookedUntil, Taken, UserId) VALUES
           (_SeatNo, _PlaneId, _FlightId, NULL, TRUE, NULL)
    ON CONFLICT(SeatNo, PlaneId, FlightId) DO UPDATE
       SET UserId = NULL,
           BookedUntil = NULL,
           Taken = TRUE;

    RETURN TRUE;
END;
$$
SELECT * FROM BuyFree(2, '002B');

5

CREATE OR REPLACE FUNCTION BuyReserved(_UserId INT, _Pass TEXT, _FlightId INT, _SeatNo VARCHAR(4))
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
DECLARE _PlaneId INT = (
    SELECT PlaneId
    FROM Flights
    WHERE FlightId = _FlightId
);
DECLARE _FlightTime TIMESTAMP = (
    SELECT FlightTime
    FROM Flights
    WHERE FlightId = _FlightId
);
DECLARE _ReservedByUser INT = (
    SELECT UserId
    FROM Tickets
    WHERE PlaneId = _PlaneId
          AND FlightId = _FlightId
          AND SeatNo = _SeatNo
);
BEGIN
    IF _PlaneId IS NULL OR _FlightTime IS NULL OR _ReservedByUser IS NULL THEN
       RETURN FALSE;
    END IF;

    IF NOT Authenticate(_UserId, _Pass) THEN
       RETURN FALSE;
    END IF;

    IF _UserId != _ReservedByUser THEN
       RETURN FALSE;
    END IF;

    IF _FlightTime < NOW() THEN
       RETURN FALSE;
    END IF;

    UPDATE Tickets
    SET BookedUntil = NULL,
        TAKEN = TRUE
    WHERE PlaneId = _PlaneId
          AND FlightId = _FlightId
          AND SeatNo = _SeatNo;

    RETURN TRUE;
END;
$$
SELECT * FROM BuyReserved(2, '123456', 5, '0001');

6

-- Статистика по местам которые можно забронировать
CREATE OR REPLACE VIEW StatCanReserve AS
SELECT FlightId, COUNT(*) AS CanReserve
FROM FreeOrReservedTickets
WHERE BookedUntil IS NULL
      OR BookedUntil < NOW()
GROUP BY FlightId;

-- Статистика по местам которые может купить пользователь
CREATE OR REPLACE FUNCTION StatCanBuy(_UserId INT)
RETURNS TABLE (
    FlightId INT,
    CanBuy INT
)
LANGUAGE plpgsql
AS $$
BEGIN
   RETURN QUERY (
         SELECT FreeOrReservedTickets.FlightId,
           CAST(COUNT(*) AS INT) AS CanBuy
      FROM FreeOrReservedTickets
      WHERE BookedUntil IS NULL
        OR BookedUntil < NOW()
        OR (
           BookedUntil > NOW()
           AND UserId = _UserId
        )
      GROUP BY FreeOrReservedTickets.FlightId
   );
END;
$$;


-- Статистика по свободным местам
CREATE OR REPLACE VIEW StatFree AS
SELECT FlightId, COUNT(*) AS Free
FROM FreeOrReservedTickets
WHERE BookedUntil IS NULL
      OR BookedUntil < NOW()
GROUP BY FlightId;


-- Статистика по забронированным местам
CREATE OR REPLACE VIEW StatReserved AS
SELECT FlightId, COUNT(*) AS Reserved
FROM FreeOrReservedTickets
WHERE BookedUntil > NOW()
GROUP BY FlightId;


-- Статистика по купленным местам
CREATE OR REPLACE VIEW StatSold AS
SELECT FlightId, COUNT(Taken = TRUE) AS Sold
FROM AllSeats
GROUP BY FlightId;
SELECT * FROM AllSeats;
SELECT * FROM StatCanBuy(3);
CREATE OR REPLACE FUNCTION FlightsStatistics(_UserId INT, _Pass TEXT)
RETURNS TABLE (
    CanReserve INT,
    CanBuy INT,
    Free INT,
    Reserved INT,
    Sold INT
)
LANGUAGE plpgsql
AS $$
BEGIN
    IF NOT Authenticate(_UserId, _Pass) THEN
       RETURN;
    END IF;

    RETURN QUERY (
         SELECT CAST(SUM(StatCanReserve.CanReserve) AS INT),
        CAST(SUM(StatCanBuy.CanBuy) AS INT),
        CAST(SUM(StatFree.Free) AS INT),
        CAST(SUM(StatReserved.Reserved) AS INT),
        CAST(SUM(StatSold.Sold) AS INT)
         FROM StatFree
             NATURAL LEFT JOIN StatReserved
             NATURAL LEFT JOIN StatSold
             NATURAL LEFT JOIN StatCanReserve
             NATURAL LEFT JOIN StatCanBuy(_UserId)
    );
END;
$$
SELECT * FROM FlightsStatistics(3, 'amogus');

7

CREATE OR REPLACE FUNCTION FlightStat(_UserId INT, _Pass TEXT, _FlightId INT)
RETURNS TABLE (
    CanReserve INT,
    CanBuy INT,
    Free INT,
    Reserved INT,
    Sold INT
)
LANGUAGE plpgsql
AS $$
BEGIN
    IF NOT Authenticate(_UserId, _Pass) THEN
       RETURN;
    END IF;

    RETURN QUERY (
         SELECT CAST(StatCanReserve.CanReserve AS INT),
        CAST(StatCanBuy.CanBuy AS INT),
        CAST(StatFree.Free AS INT),
        CAST(StatReserved.Reserved AS INT),
        CAST(StatSold.Sold AS INT)
         FROM StatFree
             NATURAL LEFT JOIN StatReserved
             NATURAL LEFT JOIN StatSold
             NATURAL LEFT JOIN StatCanReserve
             NATURAL LEFT JOIN StatCanBuy(_UserId)
     WHERE StatFree.FlightId = _FlightId
    );
END;
$$
SELECT * FROM FlightStat(2, '123456', 5);

8

CREATE OR REPLACE FUNCTION CompressSeats(_FlightId INT)
RETURNS BOOL
LANGUAGE plpgsql
AS $$
DECLARE _CurSeatNo VARCHAR(4);
DECLARE _SeatCursor CURSOR FOR
    SELECT SeatNo
    FROM Seats
         NATURAL JOIN Flights
    WHERE FlightId = _FlightId
    ORDER BY SeatNo
    FOR READ ONLY;
DECLARE _TakenCursor CURSOR FOR
    SELECT SeatNo
    FROM Tickets
    WHERE FlightId = _FlightId
          AND Taken
    FOR UPDATE;
DECLARE _ReservedCursor CURSOR FOR
    SELECT SeatNo
    FROM Tickets
    WHERE FlightId = _FlightId
          AND NOT Taken
          AND BookedUntil >= NOW()
      FOR UPDATE;
BEGIN
    SET CONSTRAINTS ALL DEFERRED;

    DELETE FROM Tickets
    WHERE NOT Taken
          AND (
           BookedUntil < NOW()
           OR BookedUntil IS NULL
          )
          AND FlightId = _FlightId;

    IF _FlightId NOT IN (SELECT FlightId FROM Flights) THEN
       RETURN FALSE;
    END IF;

    OPEN _SeatCursor;

    FOR _SeatNo IN _TakenCursor LOOP
        FETCH NEXT FROM _SeatCursor INTO _CurSeatNo;
        UPDATE Tickets
        SET SeatNo = _CurSeatNo
        WHERE CURRENT OF _TakenCursor; 
    END LOOP;

    FOR _SeatNo IN _ReservedCursor LOOP
        FETCH NEXT FROM _SeatCursor INTO _CurSeatNo;
        UPDATE Tickets
        SET SeatNo = _CurSeatNo
        WHERE CURRENT OF _ReservedCursor; 
    END LOOP;

    CLOSE _SeatCursor;

    RETURN TRUE;
END;
$$
SELECT * FROM CompressSeats(2);

HW-10 tests

START TRANSACTION READ ONLY ISOLATION LEVEL READ COMMITTED;
SELECT SeatNo, BookedUntil FROM FreeOrReservedTickets
  WHERE FlightId = 2
  AND (UserId = 1 OR UserId IS NULL);
COMMIT;

Comments

  1. FreeSeats
  2. Reserve
  3. ExtendReservation
  4. BuyFree
  5. BuyReserved
  6. FlightsStatistics
  7. FlightsStat
    • Дублирования кода подсчёта статистики
  8. CompressSeats
    • Дублирования кода подсчёта статистики