diff --git a/generator/generate.cs b/generator/generate.cs new file mode 100644 index 0000000..bbfdab8 --- /dev/null +++ b/generator/generate.cs @@ -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=ewc2025root;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 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 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(); +{ + 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(); +{ + 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(); +{ + 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(); +{ + 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(); +{ + 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(); +{ + 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(); +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(); +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(); +var roomRows = new List(); +var hotelCharRows = new List(); +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(); +{ + 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>(); +{ + 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(); +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(); +var roomBookingRows = new List(); + +// 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.");