Compare commits

..

5 Commits

Author SHA1 Message Date
e64288694b promenjeno 2026-05-17 20:56:31 +02:00
ee95be1031 skripte updateovane 2026-05-17 20:43:31 +02:00
5f6251114f generator 2026-05-17 20:43:20 +02:00
a49013fc0c new schemas 2026-05-17 20:32:58 +02:00
3cd7be1e7b nesto 2026-05-17 20:25:51 +02:00
8 changed files with 835 additions and 9 deletions

View File

@@ -1,9 +1,9 @@
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$CONTAINER = "ewc2025-mysql" $CONTAINER = "hotel-mysql"
$IMAGE = "mysql:8.4" $IMAGE = "mysql:8.4"
$ROOT_PASSWORD = "ewc2025root" $ROOT_PASSWORD = "hotel2025root"
$DATABASE = "ewc2025" $DATABASE = "hotel_reservations"
$PORT = "13306" $PORT = "13306"
$SQL_DIR = Resolve-Path "$PSScriptRoot\..\sql" $SQL_DIR = Resolve-Path "$PSScriptRoot\..\sql"

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
CONTAINER="ewc2025-mysql" CONTAINER="hotel-mysql"
IMAGE="mysql:8.4" IMAGE="mysql:8.4"
ROOT_PASSWORD="ewc2025root" ROOT_PASSWORD="hotel2025root"
DATABASE="ewc2025" DATABASE="hotel_reservations"
PORT="13306" PORT="13306"
RUNTIME="docker" RUNTIME="docker"
@@ -26,7 +26,7 @@ else
-e MYSQL_DATABASE="${DATABASE}" \ -e MYSQL_DATABASE="${DATABASE}" \
-p "${PORT}:3306" \ -p "${PORT}:3306" \
-v "${CONTAINER}-data:/var/lib/mysql" \ -v "${CONTAINER}-data:/var/lib/mysql" \
-v "${SQL_DIR}/schema.sql:/docker-entrypoint-initdb.d/01_schema.sql:ro" \ -v "${SQL_DIR}/schema.sql:/docker-entrypoint-initdb.d/01_schema.sql:ro,z" \
"${IMAGE}" "${IMAGE}"
fi fi

View File

@@ -1,6 +1,6 @@
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$CONTAINER = "ewc2025-mysql" $CONTAINER = "hotel-mysql"
$exists = docker ps -a --format '{{.Names}}' | Where-Object { $_ -eq $CONTAINER } $exists = docker ps -a --format '{{.Names}}' | Where-Object { $_ -eq $CONTAINER }

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
CONTAINER="ewc2025-mysql" CONTAINER="hotel-mysql"
RUNTIME="docker" RUNTIME="docker"
if [[ "${1:-}" == "--podman" ]]; then if [[ "${1:-}" == "--podman" ]]; then

539
generator/generate.cs Normal file
View File

@@ -0,0 +1,539 @@
#:package MySqlConnector@2.3.7
using System.Text;
using MySqlConnector;
// ── Config ────────────────────────────────────────────────────────────────────
const string DSN = "Server=127.0.0.1;Port=13306;Database=hotel_reservations;Uid=root;Pwd=hotel2025root;AllowLoadLocalInfile=true;";
const int HOTEL_COUNT = 200;
const int GUEST_COUNT = 100_000;
const int BOOKING_COUNT = 500_000;
const int BATCH = 500;
const int SEED = 42;
var rng = new Random(SEED);
// ── DB helpers ────────────────────────────────────────────────────────────────
await using var conn = new MySqlConnection(DSN);
await conn.OpenAsync();
Console.WriteLine("Connected.");
async Task Exec(string sql)
{
await using var cmd = new MySqlCommand(sql, conn);
cmd.CommandTimeout = 300;
await cmd.ExecuteNonQueryAsync();
}
async Task<long> ExecScalar(string sql)
{
await using var cmd = new MySqlCommand(sql, conn);
return Convert.ToInt64(await cmd.ExecuteScalarAsync());
}
// Bulk insert: builds a single INSERT ... VALUES (...),(...),...
async Task BulkInsert(string table, string columns, List<string> valueTuples)
{
for (int i = 0; i < valueTuples.Count; i += BATCH)
{
var batch = valueTuples.Skip(i).Take(BATCH);
await Exec($"INSERT INTO {table} ({columns}) VALUES {string.Join(',', batch)}");
}
}
string S(string? s) => s == null ? "NULL" : $"'{s.Replace("'", "''")}'";
string N(object? n) => n == null ? "NULL" : n.ToString()!;
string D(DateTime d) => $"'{d:yyyy-MM-dd}'";
string DT(DateTime d) => $"'{d:yyyy-MM-dd HH:mm:ss}'";
// ── Reference data ────────────────────────────────────────────────────────────
// ── 1. hotel_chain ────────────────────────────────────────────────────────────
Console.WriteLine("[1/8] hotel_chain");
var chains = new (string Code, string Name)[]
{
("HLT", "Hilton Worldwide"),
("MRT", "Marriott International"),
("HYT", "Hyatt Hotels Corporation"),
("IHG", "InterContinental Hotels Group"),
("WYN", "Wyndham Hotels & Resorts"),
("ACC", "Accor"),
("BW", "Best Western Hotels"),
("RAD", "Radisson Hotels"),
("MEL", "Meliá Hotels International"),
("NH", "NH Hotel Group"),
};
await BulkInsert("hotel_chain", "code, name",
chains.Select(c => $"({S(c.Code)},{S(c.Name)})").ToList());
var chainIds = new Dictionary<string, int>();
{
await using var cmd = new MySqlCommand("SELECT hotel_chain_id, code FROM hotel_chain", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) chainIds[r.GetString(1)] = r.GetInt32(0);
}
// ── 2. country ────────────────────────────────────────────────────────────────
Console.WriteLine("[2/8] country");
var countries = new (string Code, string Name, string Currency)[]
{
("GB","United Kingdom","GBP"), ("FR","France","EUR"), ("DE","Germany","EUR"),
("ES","Spain","EUR"), ("IT","Italy","EUR"), ("PT","Portugal","EUR"),
("NL","Netherlands","EUR"), ("BE","Belgium","EUR"), ("AT","Austria","EUR"),
("CH","Switzerland","CHF"), ("SE","Sweden","SEK"), ("NO","Norway","NOK"),
("DK","Denmark","DKK"), ("PL","Poland","PLN"), ("CZ","Czech Republic","CZK"),
("HU","Hungary","HUF"), ("HR","Croatia","EUR"), ("GR","Greece","EUR"),
("TR","Turkey","TRY"), ("US","United States","USD"), ("CA","Canada","CAD"),
("MX","Mexico","MXN"), ("BR","Brazil","BRL"), ("AR","Argentina","ARS"),
("AU","Australia","AUD"), ("NZ","New Zealand","NZD"), ("JP","Japan","JPY"),
("CN","China","CNY"), ("KR","South Korea","KRW"), ("SG","Singapore","SGD"),
("TH","Thailand","THB"), ("AE","United Arab Emirates","AED"),
("SA","Saudi Arabia","SAR"), ("EG","Egypt","EGP"), ("ZA","South Africa","ZAR"),
("IN","India","INR"), ("MA","Morocco","MAD"), ("TN","Tunisia","TND"),
("ID","Indonesia","IDR"), ("MY","Malaysia","MYR"),
};
await BulkInsert("country", "code, name, currency",
countries.Select(c => $"({S(c.Code)},{S(c.Name)},{S(c.Currency)})").ToList());
var countryIds = new Dictionary<string, int>();
{
await using var cmd = new MySqlCommand("SELECT country_id, code FROM country", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) countryIds[r.GetString(1)] = r.GetInt32(0);
}
// ── 3. star_rating ────────────────────────────────────────────────────────────
Console.WriteLine("[3/8] star_rating");
await BulkInsert("star_rating", "code, description",
Enumerable.Range(1, 5).Select(i => $"({i},{S(i + " Star")})").ToList());
var starIds = new Dictionary<int, int>();
{
await using var cmd = new MySqlCommand("SELECT star_rating_id, code FROM star_rating", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) starIds[r.GetInt32(1)] = r.GetInt32(0);
}
// ── 4. hotel_characteristic ───────────────────────────────────────────────────
Console.WriteLine("[4/8] hotel_characteristic");
var characteristics = new (string Code, string Desc)[]
{
("WIFI", "Free WiFi"), ("POOL", "Swimming Pool"),
("GYM", "Fitness Center"), ("SPA", "Spa & Wellness"),
("RESTAURANT", "On-site Restaurant"), ("BAR", "Hotel Bar"),
("PARKING", "Free Parking"), ("VALET", "Valet Parking"),
("CONFERENCE", "Conference Rooms"), ("SHUTTLE", "Airport Shuttle"),
("ROOM_SVC", "Room Service"), ("PETS", "Pet Friendly"),
};
await BulkInsert("hotel_characteristic", "code, description",
characteristics.Select(c => $"({S(c.Code)},{S(c.Desc)})").ToList());
var charIds = new Dictionary<string, int>();
{
await using var cmd = new MySqlCommand("SELECT characteristic_id, code FROM hotel_characteristic", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) charIds[r.GetString(1)] = r.GetInt32(0);
}
// ── 5. room_type + rate_period + period_room_rate ─────────────────────────────
Console.WriteLine("[5/8] room_type / rate_period / period_room_rate");
var roomTypes = new (string Code, string Desc, decimal BaseRate, bool Smoking)[]
{
("SINGLE", "Single Room", 80m, false),
("DOUBLE", "Double Room", 120m, false),
("TWIN", "Twin Room", 115m, false),
("DELUXE", "Deluxe Double", 180m, false),
("SUITE", "Junior Suite", 280m, false),
("EXEC", "Executive Suite", 450m, false),
("FAMILY", "Family Room", 200m, false),
};
await BulkInsert("room_type", "code, description, standard_rate, smoking_yn",
roomTypes.Select(rt => $"({S(rt.Code)},{S(rt.Desc)},{rt.BaseRate},0)").ToList());
var roomTypeIds = new Dictionary<string, int>();
{
await using var cmd = new MySqlCommand("SELECT room_type_id, code FROM room_type", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) roomTypeIds[r.GetString(1)] = r.GetInt32(0);
}
// Seasons: month → multiplier
var ratePeriods = new (string Code, string Desc, int MonthFrom, int MonthTo, decimal Multiplier)[]
{
("PEAK", "Peak Season (Jun-Aug)", 6, 8, 1.5m),
("HIGH", "High Season (Mar-May)", 3, 5, 1.2m),
("AUTUMN", "Autumn Season (Sep-Nov)", 9, 11, 1.1m),
("WINTER", "Winter Season (Dec-Feb)", 12, 2, 0.9m),
};
await BulkInsert("rate_period", "code, description, month_from, month_to",
ratePeriods.Select(rp => $"({S(rp.Code)},{S(rp.Desc)},{rp.MonthFrom},{rp.MonthTo})").ToList());
var ratePeriodIds = new Dictionary<string, int>();
{
await using var cmd = new MySqlCommand("SELECT rate_period_id, code FROM rate_period", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) ratePeriodIds[r.GetString(1)] = r.GetInt32(0);
}
// period_room_rate: rate = base_rate * season_multiplier
var prrRows = new List<string>();
foreach (var rt in roomTypes)
foreach (var rp in ratePeriods)
{
var rate = Math.Round(rt.BaseRate * rp.Multiplier, 2);
prrRows.Add($"({roomTypeIds[rt.Code]},{ratePeriodIds[rp.Code]},{rate})");
}
await BulkInsert("period_room_rate", "room_type_id, rate_period_id, rate", prrRows);
// Build month → rate lookup in memory
var monthToRatePeriodId = new Dictionary<int, int>();
foreach (var rp in ratePeriods)
{
if (rp.MonthFrom <= rp.MonthTo)
for (int m = rp.MonthFrom; m <= rp.MonthTo; m++)
monthToRatePeriodId[m] = ratePeriodIds[rp.Code];
else // wraps year (Dec-Feb)
{
for (int m = rp.MonthFrom; m <= 12; m++) monthToRatePeriodId[m] = ratePeriodIds[rp.Code];
for (int m = 1; m <= rp.MonthTo; m++) monthToRatePeriodId[m] = ratePeriodIds[rp.Code];
}
}
// Build (room_type_id, rate_period_id) → rate lookup
var rateMap = new Dictionary<(int, int), decimal>();
foreach (var rt in roomTypes)
foreach (var rp in ratePeriods)
rateMap[(roomTypeIds[rt.Code], ratePeriodIds[rp.Code])] =
Math.Round(rt.BaseRate * rp.Multiplier, 2);
// ── 6. hotel + hotel_room + hotel_hotel_characteristic ────────────────────────
Console.WriteLine("[6/8] hotel / hotel_room / hotel_hotel_characteristic");
var hotelCities = new (string City, string Country)[]
{
("London","GB"), ("Manchester","GB"), ("Edinburgh","GB"),
("Paris","FR"), ("Lyon","FR"), ("Nice","FR"),
("Berlin","DE"), ("Munich","DE"), ("Hamburg","DE"),
("Madrid","ES"), ("Barcelona","ES"), ("Seville","ES"),
("Rome","IT"), ("Milan","IT"), ("Florence","IT"),
("Lisbon","PT"), ("Porto","PT"), ("Amsterdam","NL"),
("Vienna","AT"), ("Zurich","CH"), ("Geneva","CH"),
("Stockholm","SE"), ("Oslo","NO"), ("Copenhagen","DK"),
("Warsaw","PL"), ("Prague","CZ"), ("Budapest","HU"),
("Athens","GR"), ("Istanbul","TR"), ("New York","US"),
("Los Angeles","US"), ("Miami","US"), ("Chicago","US"),
("Toronto","CA"), ("Vancouver","CA"), ("Sydney","AU"),
("Melbourne","AU"), ("Tokyo","JP"), ("Osaka","JP"),
("Singapore","SG"), ("Bangkok","TH"), ("Dubai","AE"),
("Mumbai","IN"), ("Cape Town","ZA"), ("Marrakech","MA"),
("Cairo","EG"), ("Cancun","MX"), ("Rio de Janeiro","BR"),
("Seoul","KR"), ("Kuala Lumpur","MY"),
};
// Star rating distribution: 3★ most common, 5★ rarest
int[] starWeights = [0, 5, 10, 40, 30, 15]; // index = star, value = weight
int PickStar()
{
int roll = rng.Next(100);
int cum = 0;
for (int s = 1; s <= 5; s++) { cum += starWeights[s]; if (roll < cum) return s; }
return 3;
}
var hotelRows = new List<string>();
var roomRows = new List<string>();
var hotelCharRows = new List<string>();
var charCodes = characteristics.Select(c => c.Code).ToArray();
// Track room_type per hotel_room for later rate lookups (in-memory)
// hotel_room gets an auto-increment ID; we'll load them after insert
// So store: hotel index → list of (room_number, room_type_code)
var hotelRoomTypes = new List<(int hotelIndex, string roomNumber, string roomTypeCode)>();
string[] streetNames = ["Main St","Park Ave","King Rd","Grand Blvd","Lake Dr",
"Ocean Blvd","Hill Rd","Market St","Central Ave","Palace Rd"];
for (int h = 0; h < HOTEL_COUNT; h++)
{
var (city, ctryCode) = hotelCities[h % hotelCities.Length];
int chainIndex = rng.Next(chains.Length);
// 20% of hotels are independent (no chain)
int? chainId = rng.Next(100) < 20 ? null : chainIds[chains[chainIndex].Code];
int star = PickStar();
int starId = starIds[star];
int ctryId = countryIds[ctryCode];
string code = $"HTL{h+1:D4}";
string name = chainId == null
? $"The {city} Hotel"
: $"{chains[chainIndex].Name.Split(' ')[0]} {city}";
string addr = $"{rng.Next(1, 200)} {streetNames[rng.Next(streetNames.Length)]}";
string url = $"https://www.{code.ToLower()}.example.com";
hotelRows.Add($"({N(chainId)},{ctryId},{starId},{S(code)},{S(name)},{S(addr)},{S("00000")},{S(city)},{S(url)})");
// Characteristics: 5★ gets all, lower stars get fewer
int charCount = star switch { 5 => 11, 4 => 8, 3 => 6, 2 => 4, _ => 3 };
var shuffled = charCodes.OrderBy(_ => rng.Next()).Take(charCount).ToArray();
// Store char codes for later — we need hotel_id from DB first
// Mark with h as placeholder; we'll match after insert
foreach (var cc in shuffled)
hotelCharRows.Add($"__HOTEL_{h}__,{charIds[cc]}");
// Rooms: more rooms for higher star hotels
int roomCount = star switch { 5 => rng.Next(40, 60), 4 => rng.Next(25, 40),
3 => rng.Next(15, 25), 2 => rng.Next(8, 15), _ => rng.Next(5, 10) };
// Room type distribution per star rating
string[] typePool = star switch
{
5 => ["DOUBLE","DOUBLE","DELUXE","DELUXE","SUITE","SUITE","EXEC","FAMILY"],
4 => ["SINGLE","DOUBLE","DOUBLE","DELUXE","SUITE","FAMILY"],
3 => ["SINGLE","SINGLE","DOUBLE","DOUBLE","TWIN","FAMILY"],
2 => ["SINGLE","SINGLE","DOUBLE","TWIN"],
_ => ["SINGLE","SINGLE","DOUBLE"],
};
for (int r = 0; r < roomCount; r++)
{
int floor = r / 10 + 1;
string rnum = $"{floor}{(r % 10 + 1):D2}";
string rtype = typePool[rng.Next(typePool.Length)];
// Store for later (after we get real hotel IDs from DB)
hotelRoomTypes.Add((h, rnum, rtype));
}
}
await BulkInsert("hotel",
"hotel_chain_id, country_id, star_rating_id, code, name, address, postcode, city, url",
hotelRows);
// Load hotel IDs in order
var hotelIds = new List<int>();
{
await using var cmd = new MySqlCommand("SELECT hotel_id FROM hotel ORDER BY hotel_id", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync()) hotelIds.Add(r.GetInt32(0));
}
// Now build hotel_room rows with real hotel IDs
foreach (var (hIdx, rnum, rtype) in hotelRoomTypes)
roomRows.Add($"({hotelIds[hIdx]},{roomTypeIds[rtype]},{S(rnum)},{rnum[0] - '0'})");
await BulkInsert("hotel_room", "hotel_id, room_type_id, room_number, floor", roomRows);
// hotel_hotel_characteristic — replace placeholder with real hotel_id
var hhcRows = hotelCharRows
.Select(row => {
var parts = row.Split(',');
var hIdx = int.Parse(parts[0].Replace("__HOTEL_", "").Replace("__", ""));
var charId = parts[1];
return $"({hotelIds[hIdx]},{charId})";
})
.Distinct()
.ToList();
await BulkInsert("hotel_hotel_characteristic", "hotel_id, characteristic_id", hhcRows);
// Load rooms into memory: hotel_id → list of (room_id, room_type_id)
var hotelRooms = new Dictionary<int, List<(int RoomId, int RoomTypeId)>>();
{
await using var cmd = new MySqlCommand("SELECT room_id, hotel_id, room_type_id FROM hotel_room", conn);
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync())
{
int rid = r.GetInt32(0), hid = r.GetInt32(1), rtid = r.GetInt32(2);
if (!hotelRooms.ContainsKey(hid)) hotelRooms[hid] = [];
hotelRooms[hid].Add((rid, rtid));
}
}
// ── 7. guest ──────────────────────────────────────────────────────────────────
Console.WriteLine("[7/8] guest");
string[] firstNames =
[
"James","Mary","John","Patricia","Robert","Jennifer","Michael","Linda","William","Barbara",
"David","Elizabeth","Richard","Susan","Joseph","Jessica","Thomas","Sarah","Charles","Karen",
"Luca","Sofia","Marco","Giulia","Hans","Anna","Klaus","Maria","Pierre","Marie","Jean","Claire",
"Miguel","Ana","Carlos","Carmen","Andrei","Ioana","Mihai","Elena","Tomasz","Agnieszka",
"Dimitri","Eleni","Mehmet","Fatima","Yuki","Kenji","Haruto","Yuna","Wei","Fang","Li","Mei",
"Ahmed","Layla","Omar","Nour","Raj","Priya","Arjun","Ananya","Lucas","Emma","Noah","Olivia",
"Ethan","Ava","Mason","Isabella","Liam","Sophia","Oliver","Charlotte","Elijah","Amelia",
];
string[] lastNames =
[
"Smith","Johnson","Williams","Brown","Jones","Garcia","Miller","Davis","Wilson","Moore",
"Taylor","Anderson","Thomas","Jackson","White","Harris","Martin","Thompson","Young","Lee",
"Rossi","Ferrari","Esposito","Romano","Müller","Schmidt","Fischer","Weber","Meyer","Wagner",
"Dupont","Martin","Bernard","Petit","Dubois","Moreau","Laurent","Simon","Michel","Garcia",
"Kowalski","Nowak","Wiśniewski","Wójcik","Kowalczyk","Kamiński","Lewandowski","Zieliński",
"Papadopoulos","Georgiou","Yilmaz","Kaya","Tanaka","Sato","Suzuki","Watanabe","Ito","Yamamoto",
"Wang","Li","Zhang","Liu","Chen","Yang","Huang","Zhao","Kim","Park","Lee","Choi","Patel",
"Singh","Kumar","Sharma","Gupta","Ali","Hassan","Ahmed","Mohamed","Silva","Santos","Oliveira",
];
string[] guestCities =
[
"London","Paris","Berlin","Madrid","Rome","Amsterdam","Vienna","Zurich","Brussels","Stockholm",
"New York","Los Angeles","Chicago","Houston","Phoenix","Toronto","Vancouver","Sydney","Melbourne",
"Tokyo","Seoul","Beijing","Shanghai","Singapore","Bangkok","Dubai","Mumbai","Cape Town",
"Warsaw","Prague","Budapest","Athens","Istanbul","Lisbon","Oslo","Copenhagen","Helsinki",
];
var countryList = countries.Select(c => c.Code).ToArray();
var guestRows = new List<string>();
for (int g = 0; g < GUEST_COUNT; g++)
{
string fn = firstNames[rng.Next(firstNames.Length)];
string ln = lastNames[rng.Next(lastNames.Length)];
string name = $"{fn} {ln}";
string email = $"{fn.ToLower()}.{ln.ToLower()}{rng.Next(100, 999)}@example.com";
string city = guestCities[rng.Next(guestCities.Length)];
int ctryId = countryIds[countryList[rng.Next(countryList.Length)]];
guestRows.Add($"({ctryId},{S(name)},{S(email)},{S(city)})");
}
await BulkInsert("guest", "country_id, name, email, city", guestRows);
var guestIdMin = (int)await ExecScalar("SELECT MIN(guest_id) FROM guest");
var guestIdMax = (int)await ExecScalar("SELECT MAX(guest_id) FROM guest");
// ── 8. booking + room_booking ─────────────────────────────────────────────────
Console.WriteLine("[8/8] booking + room_booking");
var dateStart = new DateTime(2022, 1, 1);
var dateEnd = new DateTime(2025, 12, 31);
int dateRange = (dateEnd - dateStart).Days;
// Seasonal weight: month → weight (higher = more bookings)
int[] monthWeight = [0, 6, 5, 7, 9, 10, 14, 16, 15, 11, 9, 7, 11]; // Jan-Dec
DateTime RandomCheckin()
{
// Rejection sampling to simulate seasonal distribution
while (true)
{
var d = dateStart.AddDays(rng.Next(dateRange));
if (rng.Next(16) < monthWeight[d.Month]) return d;
}
}
int RandomNights() => rng.Next(100) switch
{
< 30 => 1,
< 55 => 2,
< 75 => 3,
< 85 => 4,
< 92 => 5,
< 96 => rng.Next(6, 8),
_ => rng.Next(8, 15),
};
string RandomStatus() => rng.Next(100) switch
{
< 80 => "completed",
< 90 => "confirmed",
< 97 => "cancelled",
_ => "no_show",
};
// 90% single room, 8% two rooms, 2% three rooms
int RandomRoomCount() => rng.Next(100) switch { < 90 => 1, < 98 => 2, _ => 3 };
var bookingRows = new List<string>();
var roomBookingRows = new List<string>();
// We need booking_id for room_booking FK.
// Strategy: flush bookings in batches, then read back the auto-increment IDs,
// then insert room_bookings for that batch.
int bookingsDone = 0;
while (bookingsDone < BOOKING_COUNT)
{
int batchSize = Math.Min(BATCH, BOOKING_COUNT - bookingsDone);
bookingRows.Clear();
roomBookingRows.Clear();
for (int b = 0; b < batchSize; b++)
{
int guestId = guestIdMin + rng.Next(guestIdMax - guestIdMin + 1);
int hotelId = hotelIds[rng.Next(hotelIds.Count)];
DateTime checkin = RandomCheckin();
int nights = RandomNights();
DateTime checkout = checkin.AddDays(nights);
string status = RandomStatus();
DateTime created = checkin.AddDays(-rng.Next(1, 180));
bookingRows.Add($"({guestId},{hotelId},{D(checkin)},{D(checkout)},{S(status)},{DT(created)})");
}
// Insert bookings and get the first inserted ID
long firstId = await ExecScalar("SELECT AUTO_INCREMENT FROM information_schema.tables WHERE table_schema='hotel_reservations' AND table_name='booking'");
await Exec($"INSERT INTO booking (guest_id, hotel_id, date_from, date_to, status, created_at) VALUES {string.Join(',', bookingRows)}");
// Re-derive checkin/nights from the same rng sequence is impossible after the fact,
// so re-parse from inserted rows to build room_bookings
// Simpler: re-read the batch back
await using (var cmd = new MySqlCommand(
$"SELECT booking_id, hotel_id, date_from, date_to, status FROM booking WHERE booking_id >= {firstId} ORDER BY booking_id", conn))
await using (var reader = await cmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
long bookingId = reader.GetInt64(0);
int hid = reader.GetInt32(1);
DateTime dfrom = reader.GetDateTime(2);
DateTime dto = reader.GetDateTime(3);
string status = reader.GetString(4);
int nights = (dto - dfrom).Days;
if (!hotelRooms.ContainsKey(hid) || hotelRooms[hid].Count == 0) continue;
// Skip room_bookings for cancelled/no_show sometimes
if (status == "cancelled" && rng.Next(100) < 60) continue;
if (status == "no_show" && rng.Next(100) < 30) continue;
int roomCount = RandomRoomCount();
var available = hotelRooms[hid].OrderBy(_ => rng.Next()).Take(roomCount).ToList();
foreach (var (roomId, roomTypeId) in available)
{
int ratePeriodId = monthToRatePeriodId[dfrom.Month];
decimal nightly = rateMap[(roomTypeId, ratePeriodId)];
decimal total = Math.Round(nightly * nights, 2);
roomBookingRows.Add($"({bookingId},{roomId},{D(dfrom)},{D(dto)},{nightly},{total})");
}
}
}
if (roomBookingRows.Count > 0)
await Exec($"INSERT INTO room_booking (booking_id, room_id, date_from, date_to, nightly_rate, total_amount) VALUES {string.Join(',', roomBookingRows)}");
bookingsDone += batchSize;
if (bookingsDone % 10_000 == 0)
Console.WriteLine($" bookings: {bookingsDone:N0} / {BOOKING_COUNT:N0}");
}
Console.WriteLine();
Console.WriteLine("── Row counts ───────────────────────────────");
foreach (var t in new[]{"hotel_chain","country","star_rating","hotel_characteristic",
"room_type","rate_period","period_room_rate","hotel",
"hotel_room","hotel_hotel_characteristic","guest","booking","room_booking"})
{
long cnt = await ExecScalar($"SELECT COUNT(*) FROM {t}");
Console.WriteLine($" {t,-35} {cnt,10:N0}");
}
Console.WriteLine("Done.");

BIN
nesto.pdf Normal file

Binary file not shown.

133
sql/datamart_schema.sql Normal file
View File

@@ -0,0 +1,133 @@
-- =============================================================================
-- HOTEL RESERVATIONS — DATA MART (STAR SCHEMA)
-- Target: Oracle (university lab schema)
-- Based on A.24 Revenue Data Mart — Dimensional Modelling by Example
-- =============================================================================
-- -----------------------------------------------------------------------------
-- DIMENSION TABLES
-- -----------------------------------------------------------------------------
-- YYYYMMDD integer key — cheap date range predicates, no JOIN to calendar needed
CREATE TABLE DIM_DATE (
date_key NUMBER(8,0) NOT NULL,
full_date DATE NOT NULL,
year NUMBER(4,0) NOT NULL,
quarter NUMBER(1,0) NOT NULL,
month NUMBER(2,0) NOT NULL,
month_name VARCHAR2(10) NOT NULL,
week_number NUMBER(2,0) NOT NULL,
day_of_month NUMBER(2,0) NOT NULL,
day_name VARCHAR2(10) NOT NULL,
is_weekend NUMBER(1,0) NOT NULL,
is_business_day NUMBER(1,0) NOT NULL,
season VARCHAR2(10) NOT NULL, -- Peak / High / Low / Off
CONSTRAINT pk_dim_date PRIMARY KEY (date_key),
CONSTRAINT ck_dim_date_wknd CHECK (is_weekend IN (0,1)),
CONSTRAINT ck_dim_date_bday CHECK (is_business_day IN (0,1))
);
CREATE TABLE DIM_COUNTRY (
country_key NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
country_id NUMBER(10,0) NOT NULL,
code CHAR(2) NOT NULL,
name VARCHAR2(100) NOT NULL,
currency VARCHAR2(10) NOT NULL,
CONSTRAINT pk_dim_country PRIMARY KEY (country_key),
CONSTRAINT uq_dim_cntry_id UNIQUE (country_id)
);
CREATE TABLE DIM_STAR_RATING (
star_rating_key NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
star_rating_id NUMBER(10,0) NOT NULL,
code NUMBER(1,0) NOT NULL,
description VARCHAR2(20) NOT NULL,
CONSTRAINT pk_dim_star PRIMARY KEY (star_rating_key),
CONSTRAINT uq_dim_star_id UNIQUE (star_rating_id)
);
CREATE TABLE DIM_HOTEL_CHAIN (
hotel_chain_key NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
hotel_chain_id NUMBER(10,0) NOT NULL,
code VARCHAR2(10) NOT NULL,
name VARCHAR2(100) NOT NULL,
CONSTRAINT pk_dim_chain PRIMARY KEY (hotel_chain_key),
CONSTRAINT uq_dim_chain_id UNIQUE (hotel_chain_id)
);
CREATE TABLE DIM_HOTEL (
hotel_key NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
hotel_id NUMBER(10,0) NOT NULL,
hotel_chain_key NUMBER(10,0),
country_key NUMBER(10,0) NOT NULL,
star_rating_key NUMBER(10,0) NOT NULL,
code VARCHAR2(20) NOT NULL,
name VARCHAR2(150) NOT NULL,
city VARCHAR2(100) NOT NULL,
CONSTRAINT pk_dim_hotel PRIMARY KEY (hotel_key),
CONSTRAINT uq_dim_hotel_id UNIQUE (hotel_id),
CONSTRAINT fk_dh_chain FOREIGN KEY (hotel_chain_key) REFERENCES DIM_HOTEL_CHAIN (hotel_chain_key),
CONSTRAINT fk_dh_country FOREIGN KEY (country_key) REFERENCES DIM_COUNTRY (country_key),
CONSTRAINT fk_dh_star FOREIGN KEY (star_rating_key) REFERENCES DIM_STAR_RATING (star_rating_key)
);
CREATE TABLE DIM_ROOM (
room_key NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
room_id NUMBER(10,0) NOT NULL,
hotel_key NUMBER(10,0) NOT NULL,
room_number VARCHAR2(10) NOT NULL,
floor NUMBER(3,0) NOT NULL,
room_type_code VARCHAR2(20) NOT NULL,
room_type_desc VARCHAR2(100) NOT NULL,
smoking_yn NUMBER(1,0) NOT NULL,
standard_rate NUMBER(10,2) NOT NULL,
CONSTRAINT pk_dim_room PRIMARY KEY (room_key),
CONSTRAINT uq_dim_room_id UNIQUE (room_id),
CONSTRAINT fk_dr_hotel FOREIGN KEY (hotel_key) REFERENCES DIM_HOTEL (hotel_key),
CONSTRAINT ck_dim_room_smk CHECK (smoking_yn IN (0,1))
);
CREATE TABLE DIM_GUEST (
guest_key NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
guest_id NUMBER(10,0) NOT NULL,
country_key NUMBER(10,0),
name VARCHAR2(150) NOT NULL,
city VARCHAR2(100),
CONSTRAINT pk_dim_guest PRIMARY KEY (guest_key),
CONSTRAINT uq_dim_guest_id UNIQUE (guest_id),
CONSTRAINT fk_dg_country FOREIGN KEY (country_key) REFERENCES DIM_COUNTRY (country_key)
);
-- -----------------------------------------------------------------------------
-- FACT TABLE
-- -----------------------------------------------------------------------------
-- Grain: one row per room_booking
-- Revenue measures: nightly_rate, total_amount, nights_stayed
CREATE TABLE FACT_ROOM_BOOKING (
fact_id NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
-- foreign keys
hotel_key NUMBER(10,0) NOT NULL,
hotel_chain_key NUMBER(10,0),
room_key NUMBER(10,0) NOT NULL,
guest_key NUMBER(10,0) NOT NULL,
country_key NUMBER(10,0),
star_rating_key NUMBER(10,0) NOT NULL,
checkin_date_key NUMBER(8,0) NOT NULL,
checkout_date_key NUMBER(8,0) NOT NULL,
-- degenerate dimensions
booking_status VARCHAR2(20) NOT NULL,
-- measures
nights_stayed NUMBER(4,0) NOT NULL,
nightly_rate NUMBER(10,2) NOT NULL,
total_amount NUMBER(12,2) NOT NULL,
CONSTRAINT pk_fact_rb PRIMARY KEY (fact_id),
CONSTRAINT fk_frb_hotel FOREIGN KEY (hotel_key) REFERENCES DIM_HOTEL (hotel_key),
CONSTRAINT fk_frb_chain FOREIGN KEY (hotel_chain_key) REFERENCES DIM_HOTEL_CHAIN (hotel_chain_key),
CONSTRAINT fk_frb_room FOREIGN KEY (room_key) REFERENCES DIM_ROOM (room_key),
CONSTRAINT fk_frb_guest FOREIGN KEY (guest_key) REFERENCES DIM_GUEST (guest_key),
CONSTRAINT fk_frb_country FOREIGN KEY (country_key) REFERENCES DIM_COUNTRY (country_key),
CONSTRAINT fk_frb_star FOREIGN KEY (star_rating_key) REFERENCES DIM_STAR_RATING (star_rating_key),
CONSTRAINT fk_frb_checkin FOREIGN KEY (checkin_date_key) REFERENCES DIM_DATE (date_key),
CONSTRAINT fk_frb_checkout FOREIGN KEY (checkout_date_key) REFERENCES DIM_DATE (date_key)
);

154
sql/schema.sql Normal file
View File

@@ -0,0 +1,154 @@
CREATE DATABASE IF NOT EXISTS hotel_reservations
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE hotel_reservations;
-- ─────────────────────────────────────────────────────────────────────────────
-- LOOKUP / REFERENCE TABLES
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE hotel_chain (
hotel_chain_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(10) NOT NULL,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (hotel_chain_id),
UNIQUE KEY uq_chain_code (code)
);
CREATE TABLE country (
country_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code CHAR(2) NOT NULL,
name VARCHAR(100) NOT NULL,
currency VARCHAR(10) NOT NULL,
PRIMARY KEY (country_id),
UNIQUE KEY uq_country_code (code)
);
CREATE TABLE star_rating (
star_rating_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code TINYINT UNSIGNED NOT NULL,
description VARCHAR(20) NOT NULL,
PRIMARY KEY (star_rating_id),
UNIQUE KEY uq_star_code (code)
);
CREATE TABLE hotel_characteristic (
characteristic_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(20) NOT NULL,
description VARCHAR(100) NOT NULL,
PRIMARY KEY (characteristic_id),
UNIQUE KEY uq_char_code (code)
);
CREATE TABLE room_type (
room_type_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(20) NOT NULL,
description VARCHAR(100) NOT NULL,
standard_rate DECIMAL(10,2) NOT NULL,
smoking_yn BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (room_type_id),
UNIQUE KEY uq_room_type_code (code)
);
CREATE TABLE rate_period (
rate_period_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(20) NOT NULL,
description VARCHAR(50) NOT NULL,
month_from TINYINT UNSIGNED NOT NULL,
month_to TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (rate_period_id),
UNIQUE KEY uq_rate_period_code (code)
);
-- ─────────────────────────────────────────────────────────────────────────────
-- CORE ENTITIES
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE hotel (
hotel_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
hotel_chain_id INT UNSIGNED,
country_id INT UNSIGNED NOT NULL,
star_rating_id INT UNSIGNED NOT NULL,
code VARCHAR(20) NOT NULL,
name VARCHAR(150) NOT NULL,
address VARCHAR(200),
postcode VARCHAR(20),
city VARCHAR(100) NOT NULL,
url VARCHAR(200),
PRIMARY KEY (hotel_id),
UNIQUE KEY uq_hotel_code (code),
CONSTRAINT fk_hotel_chain FOREIGN KEY (hotel_chain_id) REFERENCES hotel_chain (hotel_chain_id),
CONSTRAINT fk_hotel_country FOREIGN KEY (country_id) REFERENCES country (country_id),
CONSTRAINT fk_hotel_star FOREIGN KEY (star_rating_id) REFERENCES star_rating (star_rating_id)
);
CREATE TABLE hotel_room (
room_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
hotel_id INT UNSIGNED NOT NULL,
room_type_id INT UNSIGNED NOT NULL,
room_number VARCHAR(10) NOT NULL,
floor TINYINT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (room_id),
UNIQUE KEY uq_hotel_room (hotel_id, room_number),
CONSTRAINT fk_room_hotel FOREIGN KEY (hotel_id) REFERENCES hotel (hotel_id),
CONSTRAINT fk_room_type FOREIGN KEY (room_type_id) REFERENCES room_type (room_type_id)
);
CREATE TABLE guest (
guest_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
country_id INT UNSIGNED,
name VARCHAR(150) NOT NULL,
email VARCHAR(150),
address VARCHAR(200),
city VARCHAR(100),
PRIMARY KEY (guest_id),
CONSTRAINT fk_guest_country FOREIGN KEY (country_id) REFERENCES country (country_id)
);
CREATE TABLE booking (
booking_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
guest_id INT UNSIGNED NOT NULL,
hotel_id INT UNSIGNED NOT NULL,
date_from DATE NOT NULL,
date_to DATE NOT NULL,
status ENUM('confirmed', 'cancelled', 'completed', 'no_show') NOT NULL DEFAULT 'confirmed',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (booking_id),
CONSTRAINT fk_booking_guest FOREIGN KEY (guest_id) REFERENCES guest (guest_id),
CONSTRAINT fk_booking_hotel FOREIGN KEY (hotel_id) REFERENCES hotel (hotel_id)
);
CREATE TABLE room_booking (
room_booking_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
booking_id INT UNSIGNED NOT NULL,
room_id INT UNSIGNED NOT NULL,
date_from DATE NOT NULL,
date_to DATE NOT NULL,
nightly_rate DECIMAL(10,2) NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
PRIMARY KEY (room_booking_id),
CONSTRAINT fk_rb_booking FOREIGN KEY (booking_id) REFERENCES booking (booking_id),
CONSTRAINT fk_rb_room FOREIGN KEY (room_id) REFERENCES hotel_room (room_id)
);
-- ─────────────────────────────────────────────────────────────────────────────
-- JUNCTION TABLES
-- ─────────────────────────────────────────────────────────────────────────────
CREATE TABLE hotel_hotel_characteristic (
hotel_id INT UNSIGNED NOT NULL,
characteristic_id INT UNSIGNED NOT NULL,
PRIMARY KEY (hotel_id, characteristic_id),
CONSTRAINT fk_hhc_hotel FOREIGN KEY (hotel_id) REFERENCES hotel (hotel_id),
CONSTRAINT fk_hhc_char FOREIGN KEY (characteristic_id) REFERENCES hotel_characteristic (characteristic_id)
);
CREATE TABLE period_room_rate (
room_type_id INT UNSIGNED NOT NULL,
rate_period_id INT UNSIGNED NOT NULL,
rate DECIMAL(10,2) NOT NULL,
PRIMARY KEY (room_type_id, rate_period_id),
CONSTRAINT fk_prr_type FOREIGN KEY (room_type_id) REFERENCES room_type (room_type_id),
CONSTRAINT fk_prr_period FOREIGN KEY (rate_period_id) REFERENCES rate_period (rate_period_id)
);