diff --git a/4/.gitignore b/4/.gitignore index 6f29643..57ac663 100644 --- a/4/.gitignore +++ b/4/.gitignore @@ -1 +1,2 @@ data.sql +env.sh diff --git a/4/data.js b/4/data.js index e69de29..7ec4ddf 100644 --- a/4/data.js +++ b/4/data.js @@ -0,0 +1,260 @@ +import * as crypto from 'node:crypto' +import * as fs from 'node:fs' + +const GenMaxUInt = (max) => Math.floor(Math.random() * max) +const GenInt = (from, to) => Math.floor(Math.random() * (to - from) + from) + +const male_full_name = [ + "Семенов Дмитрий Романович", + "Захаров Сергей Григорьевич", + "Панов Артём Маркович", + "Исаев Владимир Елисеевич", + "Кириллов Степан Дмитриевич", + "Никитин Георгий Егорович", + "Терехов Даниил Давидович", + "Николаев Роман Артёмович", + "Соболев Сергей Васильевич", + "Афанасьев Серафим Тимофеевич", +] + +const female_full_name = [ + "Макарова Виктория Глебовна", + "Фролова Александра Владимировна", + "Сычева Валерия Дмитриевна", + "Плотникова Милана Марковна", + "Носкова Таисия Тимуровна", + "Чернышева Алиса Михайловна", + "Михайлова Мария Яковлевна", + "Никитина Ева Владимировна", + "Лобанова Василиса Тимофеевна", + "Князева Анна Данииловна", +] + +function GenFullName() { + const sex = GenMaxUInt(2) ? 'male' : 'female' + + + let full_name + + if (sex == 'male') { + full_name = male_full_name + } else { + full_name = female_full_name + } + const l = full_name.length + const name_i = GenMaxUInt(l) + const surname_i = GenMaxUInt(l) + const middle_name_i = GenMaxUInt(l) + const [name] = full_name[name_i].split(' ') + const [, surname] = full_name[surname_i].split(' ') + const [, , middle_name] = full_name[middle_name_i].split(' ') + + return `'${name} ${surname} ${middle_name}'` +} + +const food_product_names = [ + 'хлеб', + 'картофель', + 'яйца', + 'лук', + 'чеснок', + 'соль', + 'сахар', + 'масло', + 'макароны', + 'молоко', +] + +const drug_names = [ + "парацетамол", + "ибупрофен", + "аспирин", + "левомеколь", + "активированный уголь", + "но-шпа", + "цитрамон", + "лоратадин", + "кагоцел", + "амоксициллин", +] + +const street_names = [ + "Улица Ленина", + "Проспект Мира", + "Улица Гагарина", + "Набережная Реки Фонтанки", + "Бульвар Космонавтов", + "Переулок Строителей", + "Проезд Шевченко", + "Улица 8 Марта", + "Микрорайон Солнечный", + "Шоссе Энтузиастов", +] + +function GenPassport() { + let res = '' + + for (let i = 0; i < 10; i++) { + res += GenMaxUInt(10) + } + return `'` + res.slice(0, 4) + ' ' + res.slice(4) + `'` +} + +function GenAddress() { + return `'` + street_names[GenMaxUInt(street_names.length)] + ' ' + GenMaxUInt(100) + `'` +} + +function GenPasswdHash() { + return `decode('${crypto.randomBytes(32).toString('base64')}', 'base64')` +} + +function GenEmail() { + return `'${GenMaxUInt(1000)}@mephi.ru'` +} + +function GenPhone() { + let res = `'+7` + + for (let i = 0; i < 10; i++) { + res += GenMaxUInt(10) + } + return res + `'` +} + +const billing = [ + 'сбербанк', + 'тинкофф', + 'ипб', +] + +function GenBillingAccount() { + return `'${billing[GenMaxUInt(billing.length)]}'` +} + +function GenTimestamp() { + const start = new Date(2024, 0, 1).getTime() + const end = new Date(2026, 0, 1).getTime() + + return `'${new Date(GenInt(start, end)).toISOString()}'` +} + +function GenIntervalHours(max = 6) { + return `'${GenInt(1, max)} hours'` +} + +function GenPrice(max = 1000) { + return (Math.random() * max + 10).toFixed(2) +} + +const ClassTypes = ['Theory', 'Practice'] +const RegistrationStatuses = ['NotPaid', 'Paid'] +const EquipmentUsageTypes = ['PayOnce', 'PayMonthly'] + +function Values(callback) { + let res = 'VALUES\n' + const count = 5 + + for (let i = 0; i < count; i++) { + res += callback() + + if (i == count - 1) { + res += ';\n\n' + } else { + res += ',\n' + } + } + return res +} + + +// Master +let script = 'INSERT INTO "Master" (full_name, passwd_hash, passport, readme) ' + Values(() => { + return `(${GenFullName()}, ${GenPasswdHash()}, ${GenPassport()}, 'aboba')` +}) + +// Client +script += 'INSERT INTO "Client" (full_name, passwd_hash, phone, email, billing_account) ' + Values(() => { + return `(${GenFullName()}, ${GenPasswdHash()}, ${GenPassport()}, ${GenPhone()}, ${GenBillingAccount()})` +}) + +// Studio +script += 'INSERT INTO "Studio" (address, capacity, begin_date, duration) ' + Values(() => { + return `(${GenAddress()}, ${GenInt(10, 80)}, ${GenTimestamp()}, ${GenIntervalHours(12)})` +}) + +// Course +script += 'INSERT INTO "Course" ("Master_id", name, price, duration) ' + Values(() => { + return `(${GenInt(1, 6)}, 'курс ${GenMaxUInt(100)}', ${GenPrice()}, ${GenIntervalHours(20)})` +}) + +// Class +script += 'INSERT INTO "Class" (begin_date, "Course_id", "Studio_id", name, type, duration) ' + Values(() => { + return `(${GenTimestamp()}, ${GenInt(1, 6)}, ${GenInt(1, 6)}, 'занятие ${GenMaxUInt(100)}', '${ClassTypes[GenMaxUInt(ClassTypes.length)]}', ${GenIntervalHours()})` +}) + +// Class_Master +script += `INSERT INTO "Class_Master" ("Class_begin_date", "Class_Course_id", "Master_id") + SELECT begin_date, "Course_id", ${GenInt(1, 6)} FROM "Class" LIMIT 1;\n\n` + +// Registration +script += 'INSERT INTO "Registration" ("Client_id", "Course_id", date, status) ' + Values(() => { + return `(${GenInt(6, 11)}, ${GenInt(1, 6)}, ${GenTimestamp()}, '${RegistrationStatuses[GenMaxUInt(RegistrationStatuses.length)]}')` +}) + +// FoodProductEnum +script += 'INSERT INTO "FoodProductEnum" (name, avg_price) ' + Values(() => { + return `('${food_product_names[GenMaxUInt(food_product_names.length)]}', ${GenPrice(200)})` +}) + +// FoodProduct +script += `INSERT INTO "FoodProduct" ("FoodProductEnum_id", "Class_begin_date", "Class_Course_id", buy_price, buy_date, delivery_price, delivery_date, expires_date) + SELECT ${GenInt(1, 6)}, begin_date, "Course_id", ${GenPrice()}, ${GenTimestamp()}, ${GenPrice()}, ${GenTimestamp()}, ${GenTimestamp()} FROM "Class" LIMIT 1;\n\n` + +// Equipment +script += 'INSERT INTO "Equipment" ("Studio_id", name, usage_price, usage_type, delivery_price, delivery_date) ' + Values(() => { + return `(${GenInt(1, 6)}, 'оборудование ${GenMaxUInt(100)}', ${GenPrice(300)}, '${EquipmentUsageTypes[GenMaxUInt(EquipmentUsageTypes.length)]}', ${GenPrice(200)}, ${GenTimestamp()})` +}) + +// Class_Equipment +script += `INSERT INTO "Class_Equipment" ("Class_begin_date", "Class_Course_id", "Equipment_id") + SELECT begin_date, "Course_id", ${GenInt(1, 6)} FROM "Class" LIMIT 1;\n\n` + +// DrugEnum +script += 'INSERT INTO "DrugEnum" (name, avg_price) ' + Values(() => { + return `('${drug_names[GenMaxUInt(drug_names.length)]}', ${GenPrice(500)})` +}) + +// Drug +script += 'INSERT INTO "Drug" ("DrugEnum_id", "Course_id", buy_price, buy_date, delivery_price, delivery_date, expires_date) ' + Values(() => { + return `(${GenInt(1, 6)}, ${GenInt(1, 6)}, ${GenPrice()}, ${GenTimestamp()}, ${GenPrice()}, ${GenTimestamp()}, ${GenTimestamp()})` +}) + +// ActivityLog +script += 'INSERT INTO "ActivityLog" ("Person_id", action) ' + Values(() => { + return `(${GenInt(1, 10)}, 'login')` +}) + +const tables = [ + 'Person', + 'Client', + 'Master', + 'Studio', + 'Course', + 'Class', + 'Class_Master', + 'Registration', + 'FoodProductEnum', + 'FoodProduct', + 'Equipment', + 'Class_Equipment', + 'DrugEnum', + 'Drug', + 'DrugIntolerance', + 'ActivityLog' +] + +for (const table of tables) { + script += `SELECT * FROM "${table}";\n` +} + +fs.writeFileSync('data.sql', script) diff --git a/4/deploy.sh b/4/deploy.sh index 9501abc..0d0f745 100755 --- a/4/deploy.sh +++ b/4/deploy.sh @@ -2,12 +2,6 @@ set -exu -SERVER=root@185.103.252.32 -PSQL="sudo -u postgres psql" -DB=db2026 - -scp schema.sql data.sql "$SERVER:/tmp/$DB/" -ssh $SERVER \ - "chmod -R 777 /tmp/$DB;" \ - "$PSQL -d $DB -e -q -f /tmp/$DB/schema.sql;" \ - "$PSQL -d $DB -e -q -f /tmp/$DB/data.sql;" +source env.sh +psql -e -q -f schema.sql +psql -e -q -f data.sql diff --git a/4/report.md b/4/report.md index 07cd113..31127ec 100644 --- a/4/report.md +++ b/4/report.md @@ -23,3 +23,31 @@ * SQL-скрипт начального наполнения; * скрипт генерации тестовых данных (Python или другой удобный вам язык); * инструкция по запуску. + +# schema.sql - SQL-скрипт создания схемы; +# data.js - скрипт генерации тестовых данных; +## инструкция по запуску: + +```{sh} +node data.js +``` + +## инструкция по деплою: + +создать файл env.sh + +```{sh} +export PGUSER="" +export PGDATABASE="" +export PGPASSWORD="" +export PGHOST="" +export PGPORT="" +``` + +запустить + +```{sh} +./deploy.sh +``` + +# data.sql - SQL-скрипт начального наполнения; diff --git a/4/schema.sql b/4/schema.sql index 94d8644..88127e4 100644 --- a/4/schema.sql +++ b/4/schema.sql @@ -1,10 +1,14 @@ DROP TABLE IF EXISTS "Person" CASCADE; +DROP SEQUENCE IF EXISTS "PersonId"; + + +CREATE SEQUENCE "PersonId"; CREATE TABLE "Person" ( - id int PRIMARY KEY, + id int PRIMARY KEY DEFAULT nextval('"PersonId"'), full_name text NOT NULL, - passwd_hash char(64) NOT NULL + passwd_hash bytea NOT NULL ); @@ -12,8 +16,8 @@ CREATE TABLE "Client" ( phone text NOT NULL, email text NOT NULL, billing_account text NOT NULL, - - PRIMARY KEY(id) + + PRIMARY KEY (id) ) INHERITS ("Person"); @@ -21,15 +25,17 @@ CREATE TABLE "Master" ( passport text UNIQUE NOT NULL, readme text NOT NULL, - PRIMARY KEY(id) + PRIMARY KEY (id) ) INHERITS ("Person"); DROP TABLE IF EXISTS "Studio", "Course", "Class", "Class_Master", "Registration", "FoodProductEnum", "FoodProduct", "Equipment", "Class_Equipment", "DrugEnum", "Drug", "DrugIntolerance", "ActivityLog"; +DROP TYPE IF EXISTS "ClassType", "RegistrationStatus", "EquipmentUsageType"; + CREATE TABLE "Studio" ( - id int PRIMARY KEY, + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, address text UNIQUE NOT NULL, capacity int NOT NULL CHECK (capacity > 0), begin_date timestamptz NOT NULL, @@ -38,187 +44,206 @@ CREATE TABLE "Studio" ( CREATE TABLE "Course" ( - id int PRIMARY KEY, - Master_id int, + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Master_id" int, name text NOT NULL, + price real NOT NULL CHECK (price > 0), duration interval NOT NULL, - FOREIGN KEY (Master_id) REFERENCES "Master" (id) + FOREIGN KEY ("Master_id") REFERENCES "Master" (id) ON DELETE SET NULL ON UPDATE CASCADE ); +CREATE TYPE "ClassType" AS ENUM ( + 'Theory', + 'Practice' +); + + CREATE TABLE "Class" ( begin_date timestamptz, - Course_id int, - Studio_id int, + "Course_id" int, + "Studio_id" int, name text NOT NULL, - type int NOT NULL, + type "ClassType" NOT NULL, duration interval NOT NULL, - PRIMARY KEY (begin_date, Course_id), + PRIMARY KEY (begin_date, "Course_id"), - FOREIGN KEY (Course_id) REFERENCES "Course" (id) + FOREIGN KEY ("Course_id") REFERENCES "Course" (id) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (Studio_id) REFERENCES "Studio" (id) + FOREIGN KEY ("Studio_id") REFERENCES "Studio" (id) ON DELETE SET NULL ON UPDATE CASCADE ); CREATE TABLE "Class_Master" ( - Class_begin_date timestamptz, - Class_Course_id int, - Master_id int, + "Class_begin_date" timestamptz, + "Class_Course_id" int, + "Master_id" int, - PRIMARY KEY (Class_begin_date, Class_Course_id, Master_id), + PRIMARY KEY ("Class_begin_date", "Class_Course_id", "Master_id"), - FOREIGN KEY (Master_id) REFERENCES "Master" (id) + FOREIGN KEY ("Master_id") REFERENCES "Master" (id) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (Class_begin_date, Class_Course_id) REFERENCES "Class" (begin_date, Course_id) + FOREIGN KEY ("Class_begin_date", "Class_Course_id") REFERENCES "Class" (begin_date, "Course_id") ON DELETE CASCADE ON UPDATE CASCADE ); +CREATE TYPE "RegistrationStatus" AS ENUM ( + 'NotPaid', + 'Paid' +); + + CREATE TABLE "Registration" ( - Client_id int, - Course_id int, + "Client_id" int, + "Course_id" int, date timestamptz NOT NULL, - status int NOT NULL, + status "RegistrationStatus" NOT NULL, - PRIMARY KEY (Client_id, Course_id), + PRIMARY KEY ("Client_id", "Course_id"), - FOREIGN KEY (Client_id) REFERENCES "Client" (id) + FOREIGN KEY ("Client_id") REFERENCES "Client" (id) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (Course_id) REFERENCES "Course" (id) + FOREIGN KEY ("Course_id") REFERENCES "Course" (id) ON DELETE CASCADE ON UPDATE CASCADE ); CREATE TABLE "FoodProductEnum" ( - id int PRIMARY KEY, + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, avg_price real NOT NULL ); CREATE TABLE "FoodProduct" ( - id int, - FoodProductEnum_id int, - Class_begin_date timestamptz, - Class_Course_id int, + id int GENERATED ALWAYS AS IDENTITY, + "FoodProductEnum_id" int, + "Class_begin_date" timestamptz, + "Class_Course_id" int, buy_price real NOT NULL, buy_date timestamptz NOT NULL, delivery_price real NOT NULL, delivery_date timestamptz NOT NULL, expires_date timestamptz NOT NULL, - PRIMARY KEY (id, FoodProductEnum_id), + PRIMARY KEY (id, "FoodProductEnum_id"), - FOREIGN KEY (FoodProductEnum_id) REFERENCES "FoodProductEnum" (id) + FOREIGN KEY ("FoodProductEnum_id") REFERENCES "FoodProductEnum" (id) ON DELETE RESTRICT ON UPDATE RESTRICT, - FOREIGN KEY (Class_begin_date, Class_Course_id) REFERENCES "Class" (begin_date, Course_id) + FOREIGN KEY ("Class_begin_date", "Class_Course_id") REFERENCES "Class" (begin_date, "Course_id") ON DELETE SET NULL ON UPDATE CASCADE ); +CREATE TYPE "EquipmentUsageType" AS ENUM ( + 'PayOnce', + 'PayMonthly' +); + + CREATE TABLE "Equipment" ( - id int PRIMARY KEY, - Studio_id int, + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "Studio_id" int, name text NOT NULL, usage_price real NOT NULL, - usage_type int NOT NULL, + usage_type "EquipmentUsageType" NOT NULL, delivery_price real NOT NULL, delivery_date timestamptz NOT NULL, - FOREIGN KEY (Studio_id) REFERENCES "Studio" (id) + FOREIGN KEY ("Studio_id") REFERENCES "Studio" (id) ON DELETE SET NULL ON UPDATE CASCADE ); CREATE TABLE "Class_Equipment" ( - Class_begin_date timestamptz, - Class_Course_id int, - Equipment_id int, + "Class_begin_date" timestamptz, + "Class_Course_id" int, + "Equipment_id" int, - PRIMARY KEY (Class_begin_date, Class_Course_id, Equipment_id), + PRIMARY KEY ("Class_begin_date", "Class_Course_id", "Equipment_id"), - FOREIGN KEY (Class_begin_date, Class_course_id) REFERENCES "Class" (begin_date, Course_id) + FOREIGN KEY ("Class_begin_date", "Class_Course_id") REFERENCES "Class" (begin_date, "Course_id") ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (Equipment_id) REFERENCES "Equipment" (id) + FOREIGN KEY ("Equipment_id") REFERENCES "Equipment" (id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE "DrugEnum" ( - id int PRIMARY KEY, + id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, avg_price real NOT NULL ); CREATE TABLE "Drug" ( - id int, - DrugEnum_id int, - Course_id int, + id int GENERATED ALWAYS AS IDENTITY, + "DrugEnum_id" int, + "Course_id" int, buy_price real NOT NULL, buy_date timestamptz NOT NULL, delivery_price real NOT NULL, delivery_date timestamptz NOT NULL, expires_date timestamptz NOT NULL, - PRIMARY KEY (id, DrugEnum_id), + PRIMARY KEY (id, "DrugEnum_id"), - FOREIGN KEY (DrugEnum_id) REFERENCES "DrugEnum" (id) + FOREIGN KEY ("DrugEnum_id") REFERENCES "DrugEnum" (id) ON DELETE RESTRICT ON UPDATE RESTRICT, - FOREIGN KEY (Course_id) REFERENCES "Course" (id) + FOREIGN KEY ("Course_id") REFERENCES "Course" (id) ON DELETE SET NULL ON UPDATE CASCADE ); CREATE TABLE "DrugIntolerance" ( - Person_id int, - DrugEnum_id int, + "Person_id" int, + "DrugEnum_id" int, - PRIMARY KEY (Person_id, DrugEnum_id), + PRIMARY KEY ("Person_id", "DrugEnum_id"), - FOREIGN KEY (Person_id) REFERENCES "Person" (id) + FOREIGN KEY ("Person_id") REFERENCES "Person" (id) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY (DrugEnum_id) REFERENCES "DrugEnum" (id) + FOREIGN KEY ("DrugEnum_id") REFERENCES "DrugEnum" (id) ON DELETE CASCADE ON UPDATE CASCADE ); CREATE TABLE "ActivityLog" ( - id int, - Person_id int, + id int GENERATED ALWAYS AS IDENTITY, + "Person_id" int, action text NOT NULL, - PRIMARY KEY (id, Person_id), + PRIMARY KEY (id, "Person_id"), - FOREIGN KEY (Person_id) REFERENCES "Person" (id) + FOREIGN KEY ("Person_id") REFERENCES "Person" (id) ON DELETE NO ACTION ON UPDATE CASCADE );