diff --git a/internal/housing/bench_test.go b/internal/housing/bench_test.go new file mode 100644 index 0000000..0d20e78 --- /dev/null +++ b/internal/housing/bench_test.go @@ -0,0 +1,777 @@ +package housing + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "zombiezen.com/go/sqlite/sqlitex" +) + +// setupBenchmarkDB creates a test database with sample data for benchmarking +func setupBenchmarkDB(b *testing.B) (*DatabaseHousingManager, context.Context) { + // Create truly unique database name to avoid cross-benchmark contamination + dbName := fmt.Sprintf("file:bench_%s_%d.db?mode=memory&cache=shared", b.Name(), rand.Int63()) + pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{}) + if err != nil { + b.Fatalf("Failed to create benchmark database pool: %v", err) + } + + dhm := NewDatabaseHousingManager(pool) + ctx := context.Background() + + if err := dhm.EnsureHousingTables(ctx); err != nil { + b.Fatalf("Failed to create benchmark tables: %v", err) + } + + return dhm, ctx +} + +// insertBenchmarkData inserts a large dataset for benchmarking +func insertBenchmarkData(b *testing.B, dhm *DatabaseHousingManager, ctx context.Context, houseZones, playerHouses int) { + // Insert house zones + for i := 1; i <= houseZones; i++ { + zone := &HouseZone{ + ID: int32(i), + Name: fmt.Sprintf("House Type %d", i), + ZoneID: int32(100 + i), + CostCoin: int64(50000 + i*10000), + CostStatus: int64(1000 + i*100), + UpkeepCoin: int64(5000 + i*1000), + UpkeepStatus: int64(100 + i*10), + Alignment: int8(i % 3 - 1), // -1, 0, 1 + GuildLevel: int8(i % 50), + VaultSlots: int(4 + i%4), + MaxItems: int(100 + i*50), + MaxVisitors: int(10 + i*5), + UpkeepPeriod: 604800, // 1 week + Description: fmt.Sprintf("Benchmark house type %d description", i), + } + + if err := dhm.SaveHouseZone(ctx, zone); err != nil { + b.Fatalf("Failed to insert benchmark house zone: %v", err) + } + } + + // Insert player houses + for i := 1; i <= playerHouses; i++ { + houseData := PlayerHouseData{ + CharacterID: int32(1000 + i), + HouseID: int32((i % houseZones) + 1), + InstanceID: int32(5000 + i), + UpkeepDue: time.Now().Add(time.Duration(i%168) * time.Hour), // Random within a week + EscrowCoins: int64(10000 + i*1000), + EscrowStatus: int64(200 + i*20), + Status: HouseStatusActive, + HouseName: fmt.Sprintf("House %d", i), + VisitPermission: int8(i % 3), + PublicNote: fmt.Sprintf("Welcome to house %d!", i), + PrivateNote: fmt.Sprintf("Private note %d", i), + AllowFriends: i%2 == 0, + AllowGuild: i%3 == 0, + RequireApproval: i%4 == 0, + ShowOnDirectory: i%5 != 0, + AllowDecoration: i%6 != 0, + TaxExempt: i%10 == 0, + } + + _, err := dhm.AddPlayerHouse(ctx, houseData) + if err != nil { + b.Fatalf("Failed to insert benchmark player house: %v", err) + } + } +} + +// insertHouseRelatedData inserts deposits, history, access, etc. for benchmarking +func insertHouseRelatedData(b *testing.B, dhm *DatabaseHousingManager, ctx context.Context, houseID int64, entries int) { + // Insert deposits + for i := 1; i <= entries; i++ { + deposit := HouseDeposit{ + Timestamp: time.Now().Add(-time.Duration(i) * time.Hour), + Amount: int64(1000 + i*100), + LastAmount: int64(2000 + i*100), + Status: int64(50 + i*5), + LastStatus: int64(100 + i*5), + Name: fmt.Sprintf("Player %d", i%10+1), + CharacterID: int32(2000 + i%10), + } + + if err := dhm.SaveDeposit(ctx, houseID, deposit); err != nil { + b.Fatalf("Failed to insert benchmark deposit: %v", err) + } + } + + // Insert history + for i := 1; i <= entries; i++ { + history := HouseHistory{ + Timestamp: time.Now().Add(-time.Duration(i*2) * time.Hour), + Amount: int64(500 + i*50), + Status: int64(25 + i*2), + Reason: fmt.Sprintf("Transaction %d", i), + Name: fmt.Sprintf("Player %d", i%10+1), + CharacterID: int32(2000 + i%10), + PosFlag: int8(i % 2), + Type: int(i % 5), + } + + if err := dhm.AddHistory(ctx, houseID, history); err != nil { + b.Fatalf("Failed to insert benchmark history: %v", err) + } + } + + // Insert access entries + accessList := make([]HouseAccess, 0, entries/10) // Fewer access entries + for i := 1; i <= entries/10; i++ { + access := HouseAccess{ + CharacterID: int32(3000 + i), + PlayerName: fmt.Sprintf("AccessPlayer%d", i), + AccessLevel: int8(i % 3), + Permissions: int32(i % 16), // 0-15 + GrantedBy: int32(1001), + GrantedDate: time.Now().Add(-time.Duration(i*24) * time.Hour), + ExpiresDate: time.Now().Add(time.Duration(30-i) * 24 * time.Hour), + Notes: fmt.Sprintf("Access notes for player %d", i), + } + accessList = append(accessList, access) + } + + if len(accessList) > 0 { + if err := dhm.SaveHouseAccess(ctx, houseID, accessList); err != nil { + b.Fatalf("Failed to insert benchmark access: %v", err) + } + } + + // Insert amenities + for i := 1; i <= entries/5; i++ { + amenity := HouseAmenity{ + ID: int32(i), + Type: int(i % 10), + Name: fmt.Sprintf("Amenity %d", i), + Cost: int64(1000 + i*500), + StatusCost: int64(20 + i*10), + PurchaseDate: time.Now().Add(-time.Duration(i*12) * time.Hour), + X: float32(100 + i*10), + Y: float32(200 + i*15), + Z: float32(50 + i*5), + Heading: float32(i % 360), + IsActive: i%2 == 0, + } + + if err := dhm.SaveHouseAmenity(ctx, houseID, amenity); err != nil { + b.Fatalf("Failed to insert benchmark amenity: %v", err) + } + } + + // Insert items + for i := 1; i <= entries/3; i++ { + item := HouseItem{ + ID: int64(i), + ItemID: int32(10000 + i), + CharacterID: int32(1001), + X: float32(150 + i*5), + Y: float32(250 + i*7), + Z: float32(75 + i*3), + Heading: float32(i % 360), + PitchX: float32(i % 10), + PitchY: float32(i % 5), + RollX: float32(i % 15), + RollY: float32(i % 8), + PlacedDate: time.Now().Add(-time.Duration(i*6) * time.Hour), + Quantity: int32(1 + i%5), + Condition: int8(100 - i%100), + House: fmt.Sprintf("room_%d", i%5), + } + + if err := dhm.SaveHouseItem(ctx, houseID, item); err != nil { + b.Fatalf("Failed to insert benchmark item: %v", err) + } + } +} + +// BenchmarkLoadHouseZones benchmarks loading all house zones +func BenchmarkLoadHouseZones(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 100, 0) // 100 house zones, no player houses + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + zones, err := dhm.LoadHouseZones(ctx) + if err != nil { + b.Fatalf("LoadHouseZones failed: %v", err) + } + if len(zones) != 100 { + b.Errorf("Expected 100 zones, got %d", len(zones)) + } + } +} + +// BenchmarkLoadHouseZone benchmarks loading a single house zone +func BenchmarkLoadHouseZone(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 100, 0) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + zone, err := dhm.LoadHouseZone(ctx, int32((i%100)+1)) + if err != nil { + b.Fatalf("LoadHouseZone failed: %v", err) + } + if zone == nil { + b.Error("LoadHouseZone returned nil zone") + } + } +} + +// BenchmarkSaveHouseZone benchmarks saving a house zone +func BenchmarkSaveHouseZone(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + zone := &HouseZone{ + ID: int32(1000 + i), + Name: fmt.Sprintf("Benchmark House %d", i), + ZoneID: int32(2000 + i), + CostCoin: int64(50000 + i*1000), + CostStatus: int64(1000 + i*10), + UpkeepCoin: int64(5000 + i*100), + UpkeepStatus: int64(100 + i), + Alignment: int8(i % 3 - 1), + GuildLevel: int8(i % 50), + VaultSlots: int(4 + i%4), + MaxItems: int(100 + i*10), + MaxVisitors: int(10 + i), + UpkeepPeriod: 604800, + Description: fmt.Sprintf("Benchmark description %d", i), + } + + if err := dhm.SaveHouseZone(ctx, zone); err != nil { + b.Fatalf("SaveHouseZone failed: %v", err) + } + } +} + +// BenchmarkLoadPlayerHouses benchmarks loading player houses +func BenchmarkLoadPlayerHouses(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 1000) // 10 house types, 1000 player houses + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + characterID := int32(1000 + (i%1000) + 1) + houses, err := dhm.LoadPlayerHouses(ctx, characterID) + if err != nil { + b.Fatalf("LoadPlayerHouses failed: %v", err) + } + if len(houses) != 1 { + b.Errorf("Expected 1 house for character %d, got %d", characterID, len(houses)) + } + } +} + +// BenchmarkLoadPlayerHouse benchmarks loading a single player house +func BenchmarkLoadPlayerHouse(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 100) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + houseID := int64((i % 100) + 1) + house, err := dhm.LoadPlayerHouse(ctx, houseID) + if err != nil { + b.Fatalf("LoadPlayerHouse failed: %v", err) + } + if house == nil { + b.Error("LoadPlayerHouse returned nil house") + } + } +} + +// BenchmarkAddPlayerHouse benchmarks adding player houses +func BenchmarkAddPlayerHouse(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 0) // Just house zones + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + houseData := PlayerHouseData{ + CharacterID: int32(5000 + i), + HouseID: int32((i % 10) + 1), + InstanceID: int32(10000 + i), + UpkeepDue: time.Now().Add(24 * time.Hour), + EscrowCoins: int64(25000 + i*100), + EscrowStatus: int64(500 + i*5), + Status: HouseStatusActive, + HouseName: fmt.Sprintf("Benchmark House %d", i), + VisitPermission: int8(i % 3), + PublicNote: fmt.Sprintf("Welcome to benchmark house %d", i), + PrivateNote: fmt.Sprintf("Private note %d", i), + AllowFriends: i%2 == 0, + AllowGuild: i%3 == 0, + RequireApproval: i%4 == 0, + ShowOnDirectory: i%5 != 0, + AllowDecoration: i%6 != 0, + TaxExempt: i%10 == 0, + } + + _, err := dhm.AddPlayerHouse(ctx, houseData) + if err != nil { + b.Fatalf("AddPlayerHouse failed: %v", err) + } + } +} + +// BenchmarkLoadDeposits benchmarks loading house deposits +func BenchmarkLoadDeposits(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + // Insert deposit data for house ID 1 + insertHouseRelatedData(b, dhm, ctx, 1, 500) // 500 deposits + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + deposits, err := dhm.LoadDeposits(ctx, 1) + if err != nil { + b.Fatalf("LoadDeposits failed: %v", err) + } + if len(deposits) == 0 { + b.Error("LoadDeposits returned no deposits") + } + } +} + +// BenchmarkSaveDeposit benchmarks saving deposits +func BenchmarkSaveDeposit(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + deposit := HouseDeposit{ + Timestamp: time.Now(), + Amount: int64(1000 + i*10), + LastAmount: int64(2000 + i*10), + Status: int64(50 + i), + LastStatus: int64(100 + i), + Name: fmt.Sprintf("Benchmark Player %d", i), + CharacterID: int32(2000 + i), + } + + if err := dhm.SaveDeposit(ctx, 1, deposit); err != nil { + b.Fatalf("SaveDeposit failed: %v", err) + } + } +} + +// BenchmarkLoadHistory benchmarks loading house history +func BenchmarkLoadHistory(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + // Insert history data for house ID 1 + insertHouseRelatedData(b, dhm, ctx, 1, 500) // 500 history entries + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + history, err := dhm.LoadHistory(ctx, 1) + if err != nil { + b.Fatalf("LoadHistory failed: %v", err) + } + if len(history) == 0 { + b.Error("LoadHistory returned no history") + } + } +} + +// BenchmarkAddHistory benchmarks adding history entries +func BenchmarkAddHistory(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + history := HouseHistory{ + Timestamp: time.Now(), + Amount: int64(500 + i*5), + Status: int64(25 + i), + Reason: fmt.Sprintf("Benchmark transaction %d", i), + Name: fmt.Sprintf("Benchmark Player %d", i), + CharacterID: int32(2000 + i), + PosFlag: int8(i % 2), + Type: int(i % 5), + } + + if err := dhm.AddHistory(ctx, 1, history); err != nil { + b.Fatalf("AddHistory failed: %v", err) + } + } +} + +// BenchmarkLoadHouseAccess benchmarks loading house access +func BenchmarkLoadHouseAccess(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + // Insert access data for house ID 1 + insertHouseRelatedData(b, dhm, ctx, 1, 100) // Will create 10 access entries + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + access, err := dhm.LoadHouseAccess(ctx, 1) + if err != nil { + b.Fatalf("LoadHouseAccess failed: %v", err) + } + if len(access) == 0 { + b.Error("LoadHouseAccess returned no access entries") + } + } +} + +// BenchmarkSaveHouseAccess benchmarks saving house access +func BenchmarkSaveHouseAccess(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + accessList := []HouseAccess{ + { + CharacterID: int32(4000 + i), + PlayerName: fmt.Sprintf("BenchPlayer%d", i), + AccessLevel: int8(i % 3), + Permissions: int32(i % 16), + GrantedBy: 1001, + GrantedDate: time.Now(), + ExpiresDate: time.Now().Add(30 * 24 * time.Hour), + Notes: fmt.Sprintf("Benchmark access %d", i), + }, + } + + if err := dhm.SaveHouseAccess(ctx, 1, accessList); err != nil { + b.Fatalf("SaveHouseAccess failed: %v", err) + } + } +} + +// BenchmarkLoadHouseAmenities benchmarks loading house amenities +func BenchmarkLoadHouseAmenities(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + // Insert amenity data for house ID 1 + insertHouseRelatedData(b, dhm, ctx, 1, 100) // Will create 20 amenities + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + amenities, err := dhm.LoadHouseAmenities(ctx, 1) + if err != nil { + b.Fatalf("LoadHouseAmenities failed: %v", err) + } + if len(amenities) == 0 { + b.Error("LoadHouseAmenities returned no amenities") + } + } +} + +// BenchmarkSaveHouseAmenity benchmarks saving house amenities +func BenchmarkSaveHouseAmenity(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + amenity := HouseAmenity{ + ID: int32(5000 + i), + Type: int(i % 10), + Name: fmt.Sprintf("Benchmark Amenity %d", i), + Cost: int64(1000 + i*100), + StatusCost: int64(20 + i*2), + PurchaseDate: time.Now(), + X: float32(100 + i), + Y: float32(200 + i), + Z: float32(50 + i), + Heading: float32(i % 360), + IsActive: i%2 == 0, + } + + if err := dhm.SaveHouseAmenity(ctx, 1, amenity); err != nil { + b.Fatalf("SaveHouseAmenity failed: %v", err) + } + } +} + +// BenchmarkLoadHouseItems benchmarks loading house items +func BenchmarkLoadHouseItems(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + // Insert item data for house ID 1 + insertHouseRelatedData(b, dhm, ctx, 1, 150) // Will create 50 items + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + items, err := dhm.LoadHouseItems(ctx, 1) + if err != nil { + b.Fatalf("LoadHouseItems failed: %v", err) + } + if len(items) == 0 { + b.Error("LoadHouseItems returned no items") + } + } +} + +// BenchmarkSaveHouseItem benchmarks saving house items +func BenchmarkSaveHouseItem(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 1) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + item := HouseItem{ + ID: int64(6000 + i), + ItemID: int32(20000 + i), + CharacterID: 1001, + X: float32(150 + i), + Y: float32(250 + i), + Z: float32(75 + i), + Heading: float32(i % 360), + PitchX: float32(i % 10), + PitchY: float32(i % 5), + RollX: float32(i % 15), + RollY: float32(i % 8), + PlacedDate: time.Now(), + Quantity: int32(1 + i%5), + Condition: int8(100 - i%100), + House: "benchmark", + } + + if err := dhm.SaveHouseItem(ctx, 1, item); err != nil { + b.Fatalf("SaveHouseItem failed: %v", err) + } + } +} + +// BenchmarkUpdateHouseUpkeepDue benchmarks updating house upkeep due date +func BenchmarkUpdateHouseUpkeepDue(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 100) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + houseID := int64((i % 100) + 1) + newUpkeepDue := time.Now().Add(time.Duration(i) * time.Hour) + + if err := dhm.UpdateHouseUpkeepDue(ctx, houseID, newUpkeepDue); err != nil { + b.Fatalf("UpdateHouseUpkeepDue failed: %v", err) + } + } +} + +// BenchmarkUpdateHouseEscrow benchmarks updating house escrow +func BenchmarkUpdateHouseEscrow(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 100) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + houseID := int64((i % 100) + 1) + coins := int64(50000 + i*1000) + status := int64(1000 + i*10) + + if err := dhm.UpdateHouseEscrow(ctx, houseID, coins, status); err != nil { + b.Fatalf("UpdateHouseEscrow failed: %v", err) + } + } +} + +// BenchmarkGetHousesForUpkeep benchmarks getting houses for upkeep +func BenchmarkGetHousesForUpkeep(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 1000) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + cutoffTime := time.Now().Add(time.Duration(i%200) * time.Hour) + houses, err := dhm.GetHousesForUpkeep(ctx, cutoffTime) + if err != nil { + b.Fatalf("GetHousesForUpkeep failed: %v", err) + } + _ = houses // Prevent unused variable warning + } +} + +// BenchmarkGetHouseStatistics benchmarks getting house statistics +func BenchmarkGetHouseStatistics(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 20, 2000) + + // Add some deposits and history for more realistic stats + insertHouseRelatedData(b, dhm, ctx, 1, 100) + insertHouseRelatedData(b, dhm, ctx, 2, 150) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stats, err := dhm.GetHouseStatistics(ctx) + if err != nil { + b.Fatalf("GetHouseStatistics failed: %v", err) + } + if stats.TotalHouses != 2000 { + b.Errorf("Expected 2000 total houses, got %d", stats.TotalHouses) + } + } +} + +// BenchmarkGetHouseByInstance benchmarks finding houses by instance ID +func BenchmarkGetHouseByInstance(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 1000) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + instanceID := int32(5001 + (i % 1000)) + house, err := dhm.GetHouseByInstance(ctx, instanceID) + if err != nil { + b.Fatalf("GetHouseByInstance failed: %v", err) + } + if house == nil { + b.Error("GetHouseByInstance returned nil house") + } + } +} + +// BenchmarkGetNextHouseID benchmarks getting the next house ID +func BenchmarkGetNextHouseID(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 5, 100) + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + nextID, err := dhm.GetNextHouseID(ctx) + if err != nil { + b.Fatalf("GetNextHouseID failed: %v", err) + } + if nextID <= 100 { + b.Errorf("Expected next ID > 100, got %d", nextID) + } + } +} + +// BenchmarkDeletePlayerHouse benchmarks deleting player houses +func BenchmarkDeletePlayerHouse(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + + // We need to create houses to delete in each iteration + b.ResetTimer() + + for i := 0; i < b.N; i++ { + b.StopTimer() + + // Create a house to delete + houseData := PlayerHouseData{ + CharacterID: int32(7000 + i), + HouseID: 1, + InstanceID: int32(8000 + i), + UpkeepDue: time.Now().Add(24 * time.Hour), + EscrowCoins: 25000, + EscrowStatus: 500, + Status: HouseStatusActive, + HouseName: fmt.Sprintf("DeleteMe %d", i), + VisitPermission: 1, + AllowFriends: true, + ShowOnDirectory: true, + } + + houseID, err := dhm.AddPlayerHouse(ctx, houseData) + if err != nil { + b.Fatalf("Failed to create house for deletion: %v", err) + } + + b.StartTimer() + + // Delete the house + if err := dhm.DeletePlayerHouse(ctx, houseID); err != nil { + b.Fatalf("DeletePlayerHouse failed: %v", err) + } + } +} + +// BenchmarkConcurrentReads benchmarks concurrent read operations +func BenchmarkConcurrentReads(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 100) + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + characterID := int32(1001 + (i % 100)) + houses, err := dhm.LoadPlayerHouses(ctx, characterID) + if err != nil { + b.Errorf("LoadPlayerHouses failed: %v", err) + } + if len(houses) != 1 { + b.Errorf("Expected 1 house, got %d", len(houses)) + } + i++ + } + }) +} + +// BenchmarkConcurrentWrites benchmarks concurrent write operations +func BenchmarkConcurrentWrites(b *testing.B) { + dhm, ctx := setupBenchmarkDB(b) + insertBenchmarkData(b, dhm, ctx, 10, 0) + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + houseData := PlayerHouseData{ + CharacterID: int32(10000 + i), + HouseID: int32((i % 10) + 1), + InstanceID: int32(20000 + i), + UpkeepDue: time.Now().Add(24 * time.Hour), + EscrowCoins: 25000, + EscrowStatus: 500, + Status: HouseStatusActive, + HouseName: fmt.Sprintf("Concurrent House %d", i), + VisitPermission: 1, + AllowFriends: true, + ShowOnDirectory: true, + } + + _, err := dhm.AddPlayerHouse(ctx, houseData) + if err != nil { + b.Errorf("AddPlayerHouse failed: %v", err) + } + i++ + } + }) +} \ No newline at end of file diff --git a/internal/housing/database.go b/internal/housing/database.go index 4b869dd..5a09079 100644 --- a/internal/housing/database.go +++ b/internal/housing/database.go @@ -5,105 +5,119 @@ import ( "fmt" "time" - "eq2emu/internal/database" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" ) -// DatabaseHousingManager implements HousingDatabase interface using the existing database wrapper +// DatabaseHousingManager implements HousingDatabase interface using zombiezen.com/go/sqlite type DatabaseHousingManager struct { - db *database.DB + pool *sqlitex.Pool } // NewDatabaseHousingManager creates a new database housing manager -func NewDatabaseHousingManager(db *database.DB) *DatabaseHousingManager { +func NewDatabaseHousingManager(pool *sqlitex.Pool) *DatabaseHousingManager { return &DatabaseHousingManager{ - db: db, + pool: pool, } } // LoadHouseZones retrieves all available house types from database func (dhm *DatabaseHousingManager) LoadHouseZones(ctx context.Context) ([]HouseZoneData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status, alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description FROM houses ORDER BY id` - rows, err := dhm.db.QueryContext(ctx, query) + var zones []HouseZoneData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + zone := HouseZoneData{ + ID: int32(stmt.ColumnInt64(0)), + Name: stmt.ColumnText(1), + ZoneID: int32(stmt.ColumnInt64(2)), + CostCoin: stmt.ColumnInt64(3), + CostStatus: stmt.ColumnInt64(4), + UpkeepCoin: stmt.ColumnInt64(5), + UpkeepStatus: stmt.ColumnInt64(6), + Alignment: int8(stmt.ColumnInt64(7)), + GuildLevel: int8(stmt.ColumnInt64(8)), + VaultSlots: int(stmt.ColumnInt64(9)), + MaxItems: int(stmt.ColumnInt64(10)), + MaxVisitors: int(stmt.ColumnInt64(11)), + UpkeepPeriod: int32(stmt.ColumnInt64(12)), + } + + // Handle nullable description field + if stmt.ColumnType(13) != sqlite.TypeNull { + zone.Description = stmt.ColumnText(13) + } + + zones = append(zones, zone) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query house zones: %w", err) } - defer rows.Close() - - var zones []HouseZoneData - for rows.Next() { - var zone HouseZoneData - var description *string - - err := rows.Scan( - &zone.ID, - &zone.Name, - &zone.ZoneID, - &zone.CostCoin, - &zone.CostStatus, - &zone.UpkeepCoin, - &zone.UpkeepStatus, - &zone.Alignment, - &zone.GuildLevel, - &zone.VaultSlots, - &zone.MaxItems, - &zone.MaxVisitors, - &zone.UpkeepPeriod, - &description, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan house zone row: %w", err) - } - - // Handle nullable description field - if description != nil { - zone.Description = *description - } - - zones = append(zones, zone) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating house zone rows: %w", err) - } return zones, nil } // LoadHouseZone retrieves a specific house zone from database func (dhm *DatabaseHousingManager) LoadHouseZone(ctx context.Context, houseID int32) (*HouseZoneData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status, alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description FROM houses WHERE id = ?` var zone HouseZoneData - var description *string + found := false + + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{houseID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + zone = HouseZoneData{ + ID: int32(stmt.ColumnInt64(0)), + Name: stmt.ColumnText(1), + ZoneID: int32(stmt.ColumnInt64(2)), + CostCoin: stmt.ColumnInt64(3), + CostStatus: stmt.ColumnInt64(4), + UpkeepCoin: stmt.ColumnInt64(5), + UpkeepStatus: stmt.ColumnInt64(6), + Alignment: int8(stmt.ColumnInt64(7)), + GuildLevel: int8(stmt.ColumnInt64(8)), + VaultSlots: int(stmt.ColumnInt64(9)), + MaxItems: int(stmt.ColumnInt64(10)), + MaxVisitors: int(stmt.ColumnInt64(11)), + UpkeepPeriod: int32(stmt.ColumnInt64(12)), + } + + // Handle nullable description field + if stmt.ColumnType(13) != sqlite.TypeNull { + zone.Description = stmt.ColumnText(13) + } + return nil + }, + }) - err := dhm.db.QueryRowContext(ctx, query, houseID).Scan( - &zone.ID, - &zone.Name, - &zone.ZoneID, - &zone.CostCoin, - &zone.CostStatus, - &zone.UpkeepCoin, - &zone.UpkeepStatus, - &zone.Alignment, - &zone.GuildLevel, - &zone.VaultSlots, - &zone.MaxItems, - &zone.MaxVisitors, - &zone.UpkeepPeriod, - &description, - ) if err != nil { return nil, fmt.Errorf("failed to load house zone %d: %w", houseID, err) } - // Handle nullable description field - if description != nil { - zone.Description = *description + if !found { + return nil, fmt.Errorf("house zone %d not found", houseID) } return &zone, nil @@ -111,27 +125,36 @@ func (dhm *DatabaseHousingManager) LoadHouseZone(ctx context.Context, houseID in // SaveHouseZone saves a house zone to database func (dhm *DatabaseHousingManager) SaveHouseZone(ctx context.Context, zone *HouseZone) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT OR REPLACE INTO houses (id, name, zone_id, cost_coin, cost_status, upkeep_coin, upkeep_status, alignment, guild_level, vault_slots, max_items, max_visitors, upkeep_period, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - _, err := dhm.db.ExecContext(ctx, query, - zone.ID, - zone.Name, - zone.ZoneID, - zone.CostCoin, - zone.CostStatus, - zone.UpkeepCoin, - zone.UpkeepStatus, - zone.Alignment, - zone.GuildLevel, - zone.VaultSlots, - zone.MaxItems, - zone.MaxVisitors, - zone.UpkeepPeriod, - zone.Description, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + zone.ID, + zone.Name, + zone.ZoneID, + zone.CostCoin, + zone.CostStatus, + zone.UpkeepCoin, + zone.UpkeepStatus, + zone.Alignment, + zone.GuildLevel, + zone.VaultSlots, + zone.MaxItems, + zone.MaxVisitors, + zone.UpkeepPeriod, + zone.Description, + }, + }) + if err != nil { return fmt.Errorf("failed to save house zone %d: %w", zone.ID, err) } @@ -141,7 +164,16 @@ func (dhm *DatabaseHousingManager) SaveHouseZone(ctx context.Context, zone *Hous // DeleteHouseZone removes a house zone from database func (dhm *DatabaseHousingManager) DeleteHouseZone(ctx context.Context, houseID int32) error { - _, err := dhm.db.ExecContext(ctx, "DELETE FROM houses WHERE id = ?", houseID) + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + + err = sqlitex.Execute(conn, "DELETE FROM houses WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{houseID}, + }) + if err != nil { return fmt.Errorf("failed to delete house zone %d: %w", houseID, err) } @@ -151,118 +183,120 @@ func (dhm *DatabaseHousingManager) DeleteHouseZone(ctx context.Context, houseID // LoadPlayerHouses retrieves all houses owned by a character func (dhm *DatabaseHousingManager) LoadPlayerHouses(ctx context.Context, characterID int32) ([]PlayerHouseData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE char_id = ? ORDER BY unique_id` - rows, err := dhm.db.QueryContext(ctx, query, characterID) + var houses []PlayerHouseData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{characterID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + house := PlayerHouseData{ + UniqueID: stmt.ColumnInt64(0), + CharacterID: int32(stmt.ColumnInt64(1)), + HouseID: int32(stmt.ColumnInt64(2)), + InstanceID: int32(stmt.ColumnInt64(3)), + UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), + EscrowCoins: stmt.ColumnInt64(5), + EscrowStatus: stmt.ColumnInt64(6), + Status: int8(stmt.ColumnInt64(7)), + VisitPermission: int8(stmt.ColumnInt64(9)), + AllowFriends: stmt.ColumnInt64(12) != 0, + AllowGuild: stmt.ColumnInt64(13) != 0, + RequireApproval: stmt.ColumnInt64(14) != 0, + ShowOnDirectory: stmt.ColumnInt64(15) != 0, + AllowDecoration: stmt.ColumnInt64(16) != 0, + TaxExempt: stmt.ColumnInt64(17) != 0, + } + + // Handle nullable fields + if stmt.ColumnType(8) != sqlite.TypeNull { + house.HouseName = stmt.ColumnText(8) + } + if stmt.ColumnType(10) != sqlite.TypeNull { + house.PublicNote = stmt.ColumnText(10) + } + if stmt.ColumnType(11) != sqlite.TypeNull { + house.PrivateNote = stmt.ColumnText(11) + } + + houses = append(houses, house) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query player houses for character %d: %w", characterID, err) } - defer rows.Close() - - var houses []PlayerHouseData - for rows.Next() { - var house PlayerHouseData - var upkeepDueTimestamp int64 - var houseName, publicNote, privateNote *string - - err := rows.Scan( - &house.UniqueID, - &house.CharacterID, - &house.HouseID, - &house.InstanceID, - &upkeepDueTimestamp, - &house.EscrowCoins, - &house.EscrowStatus, - &house.Status, - &houseName, - &house.VisitPermission, - &publicNote, - &privateNote, - &house.AllowFriends, - &house.AllowGuild, - &house.RequireApproval, - &house.ShowOnDirectory, - &house.AllowDecoration, - &house.TaxExempt, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan player house row: %w", err) - } - - // Convert timestamp to time - house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) - - // Handle nullable fields - if houseName != nil { - house.HouseName = *houseName - } - if publicNote != nil { - house.PublicNote = *publicNote - } - if privateNote != nil { - house.PrivateNote = *privateNote - } - - houses = append(houses, house) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating player house rows: %w", err) - } return houses, nil } // LoadPlayerHouse retrieves a specific player house func (dhm *DatabaseHousingManager) LoadPlayerHouse(ctx context.Context, uniqueID int64) (*PlayerHouseData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE unique_id = ?` var house PlayerHouseData - var upkeepDueTimestamp int64 - var houseName, publicNote, privateNote *string + found := false + + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{uniqueID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + house = PlayerHouseData{ + UniqueID: stmt.ColumnInt64(0), + CharacterID: int32(stmt.ColumnInt64(1)), + HouseID: int32(stmt.ColumnInt64(2)), + InstanceID: int32(stmt.ColumnInt64(3)), + UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), + EscrowCoins: stmt.ColumnInt64(5), + EscrowStatus: stmt.ColumnInt64(6), + Status: int8(stmt.ColumnInt64(7)), + VisitPermission: int8(stmt.ColumnInt64(9)), + AllowFriends: stmt.ColumnInt64(12) != 0, + AllowGuild: stmt.ColumnInt64(13) != 0, + RequireApproval: stmt.ColumnInt64(14) != 0, + ShowOnDirectory: stmt.ColumnInt64(15) != 0, + AllowDecoration: stmt.ColumnInt64(16) != 0, + TaxExempt: stmt.ColumnInt64(17) != 0, + } + + // Handle nullable fields + if stmt.ColumnType(8) != sqlite.TypeNull { + house.HouseName = stmt.ColumnText(8) + } + if stmt.ColumnType(10) != sqlite.TypeNull { + house.PublicNote = stmt.ColumnText(10) + } + if stmt.ColumnType(11) != sqlite.TypeNull { + house.PrivateNote = stmt.ColumnText(11) + } + return nil + }, + }) - err := dhm.db.QueryRowContext(ctx, query, uniqueID).Scan( - &house.UniqueID, - &house.CharacterID, - &house.HouseID, - &house.InstanceID, - &upkeepDueTimestamp, - &house.EscrowCoins, - &house.EscrowStatus, - &house.Status, - &houseName, - &house.VisitPermission, - &publicNote, - &privateNote, - &house.AllowFriends, - &house.AllowGuild, - &house.RequireApproval, - &house.ShowOnDirectory, - &house.AllowDecoration, - &house.TaxExempt, - ) if err != nil { return nil, fmt.Errorf("failed to load player house %d: %w", uniqueID, err) } - // Convert timestamp to time - house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) - - // Handle nullable fields - if houseName != nil { - house.HouseName = *houseName - } - if publicNote != nil { - house.PublicNote = *publicNote - } - if privateNote != nil { - house.PrivateNote = *privateNote + if !found { + return nil, fmt.Errorf("player house %d not found", uniqueID) } return &house, nil @@ -270,6 +304,12 @@ func (dhm *DatabaseHousingManager) LoadPlayerHouse(ctx context.Context, uniqueID // SavePlayerHouse saves a player house to database func (dhm *DatabaseHousingManager) SavePlayerHouse(ctx context.Context, house *PlayerHouse) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT OR REPLACE INTO character_houses (unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, @@ -278,26 +318,29 @@ func (dhm *DatabaseHousingManager) SavePlayerHouse(ctx context.Context, house *P upkeepDueTimestamp := house.UpkeepDue.Unix() - _, err := dhm.db.ExecContext(ctx, query, - house.UniqueID, - house.CharacterID, - house.HouseID, - house.InstanceID, - upkeepDueTimestamp, - house.EscrowCoins, - house.EscrowStatus, - house.Status, - house.Settings.HouseName, - house.Settings.VisitPermission, - house.Settings.PublicNote, - house.Settings.PrivateNote, - house.Settings.AllowFriends, - house.Settings.AllowGuild, - house.Settings.RequireApproval, - house.Settings.ShowOnDirectory, - house.Settings.AllowDecoration, - house.Settings.TaxExempt, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + house.UniqueID, + house.CharacterID, + house.HouseID, + house.InstanceID, + upkeepDueTimestamp, + house.EscrowCoins, + house.EscrowStatus, + house.Status, + house.Settings.HouseName, + house.Settings.VisitPermission, + house.Settings.PublicNote, + house.Settings.PrivateNote, + boolToInt(house.Settings.AllowFriends), + boolToInt(house.Settings.AllowGuild), + boolToInt(house.Settings.RequireApproval), + boolToInt(house.Settings.ShowOnDirectory), + boolToInt(house.Settings.AllowDecoration), + boolToInt(house.Settings.TaxExempt), + }, + }) + if err != nil { return fmt.Errorf("failed to save player house %d: %w", house.UniqueID, err) } @@ -307,12 +350,14 @@ func (dhm *DatabaseHousingManager) SavePlayerHouse(ctx context.Context, house *P // DeletePlayerHouse removes a player house from database func (dhm *DatabaseHousingManager) DeletePlayerHouse(ctx context.Context, uniqueID int64) error { - // Use a transaction to delete house and all related data - tx, err := dhm.db.BeginTx(ctx, nil) + conn, err := dhm.pool.Take(ctx) if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) + return fmt.Errorf("failed to get database connection: %w", err) } - defer tx.Rollback() + defer dhm.pool.Put(conn) + + // Use a savepoint for nested transaction support + defer sqlitex.Save(conn)(&err) // Delete related data first tables := []string{ @@ -325,27 +370,33 @@ func (dhm *DatabaseHousingManager) DeletePlayerHouse(ctx context.Context, unique for _, table := range tables { query := fmt.Sprintf("DELETE FROM %s WHERE house_id = ?", table) - _, err = tx.ExecContext(ctx, query, uniqueID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{uniqueID}, + }) if err != nil { return fmt.Errorf("failed to delete from %s: %w", table, err) } } // Delete the house itself - _, err = tx.ExecContext(ctx, "DELETE FROM character_houses WHERE unique_id = ?", uniqueID) + err = sqlitex.Execute(conn, "DELETE FROM character_houses WHERE unique_id = ?", &sqlitex.ExecOptions{ + Args: []any{uniqueID}, + }) if err != nil { return fmt.Errorf("failed to delete player house: %w", err) } - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - return nil } // AddPlayerHouse creates a new player house entry func (dhm *DatabaseHousingManager) AddPlayerHouse(ctx context.Context, houseData PlayerHouseData) (int64, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT INTO character_houses (char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, @@ -354,97 +405,101 @@ func (dhm *DatabaseHousingManager) AddPlayerHouse(ctx context.Context, houseData upkeepDueTimestamp := houseData.UpkeepDue.Unix() - result, err := dhm.db.ExecContext(ctx, query, - houseData.CharacterID, - houseData.HouseID, - houseData.InstanceID, - upkeepDueTimestamp, - houseData.EscrowCoins, - houseData.EscrowStatus, - houseData.Status, - houseData.HouseName, - houseData.VisitPermission, - houseData.PublicNote, - houseData.PrivateNote, - houseData.AllowFriends, - houseData.AllowGuild, - houseData.RequireApproval, - houseData.ShowOnDirectory, - houseData.AllowDecoration, - houseData.TaxExempt, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + houseData.CharacterID, + houseData.HouseID, + houseData.InstanceID, + upkeepDueTimestamp, + houseData.EscrowCoins, + houseData.EscrowStatus, + houseData.Status, + houseData.HouseName, + houseData.VisitPermission, + houseData.PublicNote, + houseData.PrivateNote, + boolToInt(houseData.AllowFriends), + boolToInt(houseData.AllowGuild), + boolToInt(houseData.RequireApproval), + boolToInt(houseData.ShowOnDirectory), + boolToInt(houseData.AllowDecoration), + boolToInt(houseData.TaxExempt), + }, + }) + if err != nil { return 0, fmt.Errorf("failed to create player house: %w", err) } - id, err := result.LastInsertId() - if err != nil { - return 0, fmt.Errorf("failed to get new house ID: %w", err) - } - - return id, nil + // Get the last inserted ID + return conn.LastInsertRowID(), nil } // LoadDeposits retrieves deposit history for a house func (dhm *DatabaseHousingManager) LoadDeposits(ctx context.Context, houseID int64) ([]HouseDepositData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT house_id, timestamp, amount, last_amount, status, last_status, name, character_id FROM character_house_deposits WHERE house_id = ? ORDER BY timestamp DESC LIMIT ?` - rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxDepositHistory) + var deposits []HouseDepositData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{houseID, MaxDepositHistory}, + ResultFunc: func(stmt *sqlite.Stmt) error { + deposit := HouseDepositData{ + HouseID: stmt.ColumnInt64(0), + Timestamp: time.Unix(stmt.ColumnInt64(1), 0), + Amount: stmt.ColumnInt64(2), + LastAmount: stmt.ColumnInt64(3), + Status: stmt.ColumnInt64(4), + LastStatus: stmt.ColumnInt64(5), + Name: stmt.ColumnText(6), + CharacterID: int32(stmt.ColumnInt64(7)), + } + deposits = append(deposits, deposit) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query house deposits for house %d: %w", houseID, err) } - defer rows.Close() - - var deposits []HouseDepositData - for rows.Next() { - var deposit HouseDepositData - var timestampUnix int64 - - err := rows.Scan( - &deposit.HouseID, - ×tampUnix, - &deposit.Amount, - &deposit.LastAmount, - &deposit.Status, - &deposit.LastStatus, - &deposit.Name, - &deposit.CharacterID, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan deposit row: %w", err) - } - - deposit.Timestamp = time.Unix(timestampUnix, 0) - deposits = append(deposits, deposit) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating deposit rows: %w", err) - } return deposits, nil } // SaveDeposit saves a deposit record func (dhm *DatabaseHousingManager) SaveDeposit(ctx context.Context, houseID int64, deposit HouseDeposit) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT INTO character_house_deposits (house_id, timestamp, amount, last_amount, status, last_status, name, character_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` timestampUnix := deposit.Timestamp.Unix() - _, err := dhm.db.ExecContext(ctx, query, - houseID, - timestampUnix, - deposit.Amount, - deposit.LastAmount, - deposit.Status, - deposit.LastStatus, - deposit.Name, - deposit.CharacterID, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + houseID, + timestampUnix, + deposit.Amount, + deposit.LastAmount, + deposit.Status, + deposit.LastStatus, + deposit.Name, + deposit.CharacterID, + }, + }) + if err != nil { return fmt.Errorf("failed to save house deposit: %w", err) } @@ -454,66 +509,71 @@ func (dhm *DatabaseHousingManager) SaveDeposit(ctx context.Context, houseID int6 // LoadHistory retrieves transaction history for a house func (dhm *DatabaseHousingManager) LoadHistory(ctx context.Context, houseID int64) ([]HouseHistoryData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type FROM character_house_history WHERE house_id = ? ORDER BY timestamp DESC LIMIT ?` - rows, err := dhm.db.QueryContext(ctx, query, houseID, MaxTransactionHistory) + var history []HouseHistoryData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{houseID, MaxTransactionHistory}, + ResultFunc: func(stmt *sqlite.Stmt) error { + entry := HouseHistoryData{ + HouseID: stmt.ColumnInt64(0), + Timestamp: time.Unix(stmt.ColumnInt64(1), 0), + Amount: stmt.ColumnInt64(2), + Status: stmt.ColumnInt64(3), + Reason: stmt.ColumnText(4), + Name: stmt.ColumnText(5), + CharacterID: int32(stmt.ColumnInt64(6)), + PosFlag: int8(stmt.ColumnInt64(7)), + Type: int(stmt.ColumnInt64(8)), + } + history = append(history, entry) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query house history for house %d: %w", houseID, err) } - defer rows.Close() - - var history []HouseHistoryData - for rows.Next() { - var entry HouseHistoryData - var timestampUnix int64 - - err := rows.Scan( - &entry.HouseID, - ×tampUnix, - &entry.Amount, - &entry.Status, - &entry.Reason, - &entry.Name, - &entry.CharacterID, - &entry.PosFlag, - &entry.Type, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan history row: %w", err) - } - - entry.Timestamp = time.Unix(timestampUnix, 0) - history = append(history, entry) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating history rows: %w", err) - } return history, nil } // AddHistory adds a new history entry func (dhm *DatabaseHousingManager) AddHistory(ctx context.Context, houseID int64, history HouseHistory) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT INTO character_house_history (house_id, timestamp, amount, status, reason, name, character_id, pos_flag, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` timestampUnix := history.Timestamp.Unix() - _, err := dhm.db.ExecContext(ctx, query, - houseID, - timestampUnix, - history.Amount, - history.Status, - history.Reason, - history.Name, - history.CharacterID, - history.PosFlag, - history.Type, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + houseID, + timestampUnix, + history.Amount, + history.Status, + history.Reason, + history.Name, + history.CharacterID, + history.PosFlag, + history.Type, + }, + }) + if err != nil { return fmt.Errorf("failed to add house history: %w", err) } @@ -523,65 +583,62 @@ func (dhm *DatabaseHousingManager) AddHistory(ctx context.Context, houseID int64 // LoadHouseAccess retrieves access permissions for a house func (dhm *DatabaseHousingManager) LoadHouseAccess(ctx context.Context, houseID int64) ([]HouseAccessData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT house_id, character_id, player_name, access_level, permissions, granted_by, granted_date, expires_date, notes FROM character_house_access WHERE house_id = ?` - rows, err := dhm.db.QueryContext(ctx, query, houseID) + var accessList []HouseAccessData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{houseID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + access := HouseAccessData{ + HouseID: stmt.ColumnInt64(0), + CharacterID: int32(stmt.ColumnInt64(1)), + PlayerName: stmt.ColumnText(2), + AccessLevel: int8(stmt.ColumnInt64(3)), + Permissions: int32(stmt.ColumnInt64(4)), + GrantedBy: int32(stmt.ColumnInt64(5)), + GrantedDate: time.Unix(stmt.ColumnInt64(6), 0), + ExpiresDate: time.Unix(stmt.ColumnInt64(7), 0), + } + + if stmt.ColumnType(8) != sqlite.TypeNull { + access.Notes = stmt.ColumnText(8) + } + + accessList = append(accessList, access) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query house access for house %d: %w", houseID, err) } - defer rows.Close() - - var accessList []HouseAccessData - for rows.Next() { - var access HouseAccessData - var grantedDateUnix, expiresDateUnix int64 - var notes *string - - err := rows.Scan( - &access.HouseID, - &access.CharacterID, - &access.PlayerName, - &access.AccessLevel, - &access.Permissions, - &access.GrantedBy, - &grantedDateUnix, - &expiresDateUnix, - ¬es, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan access row: %w", err) - } - - access.GrantedDate = time.Unix(grantedDateUnix, 0) - access.ExpiresDate = time.Unix(expiresDateUnix, 0) - - if notes != nil { - access.Notes = *notes - } - - accessList = append(accessList, access) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating access rows: %w", err) - } return accessList, nil } // SaveHouseAccess saves access permissions for a house func (dhm *DatabaseHousingManager) SaveHouseAccess(ctx context.Context, houseID int64, accessList []HouseAccess) error { - // Use a transaction for atomic updates - tx, err := dhm.db.BeginTx(ctx, nil) + conn, err := dhm.pool.Take(ctx) if err != nil { - return fmt.Errorf("failed to begin transaction: %w", err) + return fmt.Errorf("failed to get database connection: %w", err) } - defer tx.Rollback() + defer dhm.pool.Put(conn) + + // Use a savepoint for nested transaction support + defer sqlitex.Save(conn)(&err) // Delete existing access for this house - _, err = tx.ExecContext(ctx, "DELETE FROM character_house_access WHERE house_id = ?", houseID) + err = sqlitex.Execute(conn, "DELETE FROM character_house_access WHERE house_id = ?", &sqlitex.ExecOptions{ + Args: []any{houseID}, + }) if err != nil { return fmt.Errorf("failed to delete existing house access: %w", err) } @@ -596,34 +653,41 @@ func (dhm *DatabaseHousingManager) SaveHouseAccess(ctx context.Context, houseID grantedDateUnix := access.GrantedDate.Unix() expiresDateUnix := access.ExpiresDate.Unix() - _, err = tx.ExecContext(ctx, insertQuery, - houseID, - access.CharacterID, - access.PlayerName, - access.AccessLevel, - access.Permissions, - access.GrantedBy, - grantedDateUnix, - expiresDateUnix, - access.Notes, - ) + err = sqlitex.Execute(conn, insertQuery, &sqlitex.ExecOptions{ + Args: []any{ + houseID, + access.CharacterID, + access.PlayerName, + access.AccessLevel, + access.Permissions, + access.GrantedBy, + grantedDateUnix, + expiresDateUnix, + access.Notes, + }, + }) if err != nil { return fmt.Errorf("failed to insert house access for character %d: %w", access.CharacterID, err) } } - if err := tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - return nil } // DeleteHouseAccess removes access for a specific character func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseID int64, characterID int32) error { - _, err := dhm.db.ExecContext(ctx, + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + + err = sqlitex.Execute(conn, "DELETE FROM character_house_access WHERE house_id = ? AND character_id = ?", - houseID, characterID) + &sqlitex.ExecOptions{ + Args: []any{houseID, characterID}, + }) + if err != nil { return fmt.Errorf("failed to delete house access: %w", err) } @@ -631,75 +695,78 @@ func (dhm *DatabaseHousingManager) DeleteHouseAccess(ctx context.Context, houseI return nil } -// Additional database methods continued in next part due to length... - // LoadHouseAmenities retrieves amenities for a house func (dhm *DatabaseHousingManager) LoadHouseAmenities(ctx context.Context, houseID int64) ([]HouseAmenityData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT house_id, id, type, name, cost, status_cost, purchase_date, x, y, z, heading, is_active FROM character_house_amenities WHERE house_id = ?` - rows, err := dhm.db.QueryContext(ctx, query, houseID) + var amenities []HouseAmenityData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{houseID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + amenity := HouseAmenityData{ + HouseID: stmt.ColumnInt64(0), + ID: int32(stmt.ColumnInt64(1)), + Type: int(stmt.ColumnInt64(2)), + Name: stmt.ColumnText(3), + Cost: stmt.ColumnInt64(4), + StatusCost: stmt.ColumnInt64(5), + PurchaseDate: time.Unix(stmt.ColumnInt64(6), 0), + X: float32(stmt.ColumnFloat(7)), + Y: float32(stmt.ColumnFloat(8)), + Z: float32(stmt.ColumnFloat(9)), + Heading: float32(stmt.ColumnFloat(10)), + IsActive: stmt.ColumnInt64(11) != 0, + } + amenities = append(amenities, amenity) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query house amenities for house %d: %w", houseID, err) } - defer rows.Close() - - var amenities []HouseAmenityData - for rows.Next() { - var amenity HouseAmenityData - var purchaseDateUnix int64 - - err := rows.Scan( - &amenity.HouseID, - &amenity.ID, - &amenity.Type, - &amenity.Name, - &amenity.Cost, - &amenity.StatusCost, - &purchaseDateUnix, - &amenity.X, - &amenity.Y, - &amenity.Z, - &amenity.Heading, - &amenity.IsActive, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan amenity row: %w", err) - } - - amenity.PurchaseDate = time.Unix(purchaseDateUnix, 0) - amenities = append(amenities, amenity) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating amenity rows: %w", err) - } return amenities, nil } // SaveHouseAmenity saves a house amenity func (dhm *DatabaseHousingManager) SaveHouseAmenity(ctx context.Context, houseID int64, amenity HouseAmenity) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT OR REPLACE INTO character_house_amenities (house_id, id, type, name, cost, status_cost, purchase_date, x, y, z, heading, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` purchaseDateUnix := amenity.PurchaseDate.Unix() - _, err := dhm.db.ExecContext(ctx, query, - houseID, - amenity.ID, - amenity.Type, - amenity.Name, - amenity.Cost, - amenity.StatusCost, - purchaseDateUnix, - amenity.X, - amenity.Y, - amenity.Z, - amenity.Heading, - amenity.IsActive, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + houseID, + amenity.ID, + amenity.Type, + amenity.Name, + amenity.Cost, + amenity.StatusCost, + purchaseDateUnix, + amenity.X, + amenity.Y, + amenity.Z, + amenity.Heading, + boolToInt(amenity.IsActive), + }, + }) + if err != nil { return fmt.Errorf("failed to save house amenity: %w", err) } @@ -709,9 +776,18 @@ func (dhm *DatabaseHousingManager) SaveHouseAmenity(ctx context.Context, houseID // DeleteHouseAmenity removes a house amenity func (dhm *DatabaseHousingManager) DeleteHouseAmenity(ctx context.Context, houseID int64, amenityID int32) error { - _, err := dhm.db.ExecContext(ctx, + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + + err = sqlitex.Execute(conn, "DELETE FROM character_house_amenities WHERE house_id = ? AND id = ?", - houseID, amenityID) + &sqlitex.ExecOptions{ + Args: []any{houseID, amenityID}, + }) + if err != nil { return fmt.Errorf("failed to delete house amenity: %w", err) } @@ -721,56 +797,58 @@ func (dhm *DatabaseHousingManager) DeleteHouseAmenity(ctx context.Context, house // LoadHouseItems retrieves items placed in a house func (dhm *DatabaseHousingManager) LoadHouseItems(ctx context.Context, houseID int64) ([]HouseItemData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT house_id, id, item_id, character_id, x, y, z, heading, pitch_x, pitch_y, roll_x, roll_y, placed_date, quantity, condition, house FROM character_house_items WHERE house_id = ?` - rows, err := dhm.db.QueryContext(ctx, query, houseID) + var items []HouseItemData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{houseID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + item := HouseItemData{ + HouseID: stmt.ColumnInt64(0), + ID: stmt.ColumnInt64(1), + ItemID: int32(stmt.ColumnInt64(2)), + CharacterID: int32(stmt.ColumnInt64(3)), + X: float32(stmt.ColumnFloat(4)), + Y: float32(stmt.ColumnFloat(5)), + Z: float32(stmt.ColumnFloat(6)), + Heading: float32(stmt.ColumnFloat(7)), + PitchX: float32(stmt.ColumnFloat(8)), + PitchY: float32(stmt.ColumnFloat(9)), + RollX: float32(stmt.ColumnFloat(10)), + RollY: float32(stmt.ColumnFloat(11)), + PlacedDate: time.Unix(stmt.ColumnInt64(12), 0), + Quantity: int32(stmt.ColumnInt64(13)), + Condition: int8(stmt.ColumnInt64(14)), + House: stmt.ColumnText(15), + } + items = append(items, item) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query house items for house %d: %w", houseID, err) } - defer rows.Close() - - var items []HouseItemData - for rows.Next() { - var item HouseItemData - var placedDateUnix int64 - - err := rows.Scan( - &item.HouseID, - &item.ID, - &item.ItemID, - &item.CharacterID, - &item.X, - &item.Y, - &item.Z, - &item.Heading, - &item.PitchX, - &item.PitchY, - &item.RollX, - &item.RollY, - &placedDateUnix, - &item.Quantity, - &item.Condition, - &item.House, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan item row: %w", err) - } - - item.PlacedDate = time.Unix(placedDateUnix, 0) - items = append(items, item) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating item rows: %w", err) - } return items, nil } // SaveHouseItem saves a house item func (dhm *DatabaseHousingManager) SaveHouseItem(ctx context.Context, houseID int64, item HouseItem) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `INSERT OR REPLACE INTO character_house_items (house_id, id, item_id, character_id, x, y, z, heading, pitch_x, pitch_y, roll_x, roll_y, placed_date, quantity, condition, house) @@ -778,24 +856,27 @@ func (dhm *DatabaseHousingManager) SaveHouseItem(ctx context.Context, houseID in placedDateUnix := item.PlacedDate.Unix() - _, err := dhm.db.ExecContext(ctx, query, - houseID, - item.ID, - item.ItemID, - item.CharacterID, - item.X, - item.Y, - item.Z, - item.Heading, - item.PitchX, - item.PitchY, - item.RollX, - item.RollY, - placedDateUnix, - item.Quantity, - item.Condition, - item.House, - ) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{ + houseID, + item.ID, + item.ItemID, + item.CharacterID, + item.X, + item.Y, + item.Z, + item.Heading, + item.PitchX, + item.PitchY, + item.RollX, + item.RollY, + placedDateUnix, + item.Quantity, + item.Condition, + item.House, + }, + }) + if err != nil { return fmt.Errorf("failed to save house item: %w", err) } @@ -805,9 +886,18 @@ func (dhm *DatabaseHousingManager) SaveHouseItem(ctx context.Context, houseID in // DeleteHouseItem removes a house item func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID int64, itemID int64) error { - _, err := dhm.db.ExecContext(ctx, + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + + err = sqlitex.Execute(conn, "DELETE FROM character_house_items WHERE house_id = ? AND id = ?", - houseID, itemID) + &sqlitex.ExecOptions{ + Args: []any{houseID, itemID}, + }) + if err != nil { return fmt.Errorf("failed to delete house item: %w", err) } @@ -815,14 +905,24 @@ func (dhm *DatabaseHousingManager) DeleteHouseItem(ctx context.Context, houseID return nil } -// Utility operations - // GetNextHouseID returns the next available house unique ID func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return 0, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := "SELECT COALESCE(MAX(unique_id), 0) + 1 FROM character_houses" var nextID int64 - err := dhm.db.QueryRowContext(ctx, query).Scan(&nextID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + nextID = stmt.ColumnInt64(0) + return nil + }, + }) + if err != nil { return 0, fmt.Errorf("failed to get next house ID: %w", err) } @@ -832,50 +932,62 @@ func (dhm *DatabaseHousingManager) GetNextHouseID(ctx context.Context) (int64, e // GetHouseByInstance finds a house by instance ID func (dhm *DatabaseHousingManager) GetHouseByInstance(ctx context.Context, instanceID int32) (*PlayerHouseData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt FROM character_houses WHERE instance_id = ?` var house PlayerHouseData - var upkeepDueTimestamp int64 - var houseName, publicNote, privateNote *string + found := false + + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{instanceID}, + ResultFunc: func(stmt *sqlite.Stmt) error { + found = true + house = PlayerHouseData{ + UniqueID: stmt.ColumnInt64(0), + CharacterID: int32(stmt.ColumnInt64(1)), + HouseID: int32(stmt.ColumnInt64(2)), + InstanceID: int32(stmt.ColumnInt64(3)), + UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), + EscrowCoins: stmt.ColumnInt64(5), + EscrowStatus: stmt.ColumnInt64(6), + Status: int8(stmt.ColumnInt64(7)), + VisitPermission: int8(stmt.ColumnInt64(9)), + AllowFriends: stmt.ColumnInt64(12) != 0, + AllowGuild: stmt.ColumnInt64(13) != 0, + RequireApproval: stmt.ColumnInt64(14) != 0, + ShowOnDirectory: stmt.ColumnInt64(15) != 0, + AllowDecoration: stmt.ColumnInt64(16) != 0, + TaxExempt: stmt.ColumnInt64(17) != 0, + } + + // Handle nullable fields + if stmt.ColumnType(8) != sqlite.TypeNull { + house.HouseName = stmt.ColumnText(8) + } + if stmt.ColumnType(10) != sqlite.TypeNull { + house.PublicNote = stmt.ColumnText(10) + } + if stmt.ColumnType(11) != sqlite.TypeNull { + house.PrivateNote = stmt.ColumnText(11) + } + return nil + }, + }) - err := dhm.db.QueryRowContext(ctx, query, instanceID).Scan( - &house.UniqueID, - &house.CharacterID, - &house.HouseID, - &house.InstanceID, - &upkeepDueTimestamp, - &house.EscrowCoins, - &house.EscrowStatus, - &house.Status, - &houseName, - &house.VisitPermission, - &publicNote, - &privateNote, - &house.AllowFriends, - &house.AllowGuild, - &house.RequireApproval, - &house.ShowOnDirectory, - &house.AllowDecoration, - &house.TaxExempt, - ) if err != nil { return nil, fmt.Errorf("failed to load house by instance %d: %w", instanceID, err) } - // Convert timestamp and handle nullable fields - house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) - - if houseName != nil { - house.HouseName = *houseName - } - if publicNote != nil { - house.PublicNote = *publicNote - } - if privateNote != nil { - house.PrivateNote = *privateNote + if !found { + return nil, fmt.Errorf("house with instance %d not found", instanceID) } return &house, nil @@ -883,10 +995,19 @@ func (dhm *DatabaseHousingManager) GetHouseByInstance(ctx context.Context, insta // UpdateHouseUpkeepDue updates the upkeep due date for a house func (dhm *DatabaseHousingManager) UpdateHouseUpkeepDue(ctx context.Context, houseID int64, upkeepDue time.Time) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := "UPDATE character_houses SET upkeep_due = ? WHERE unique_id = ?" upkeepDueTimestamp := upkeepDue.Unix() - _, err := dhm.db.ExecContext(ctx, query, upkeepDueTimestamp, houseID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{upkeepDueTimestamp, houseID}, + }) + if err != nil { return fmt.Errorf("failed to update house upkeep due: %w", err) } @@ -896,9 +1017,18 @@ func (dhm *DatabaseHousingManager) UpdateHouseUpkeepDue(ctx context.Context, hou // UpdateHouseEscrow updates the escrow balances for a house func (dhm *DatabaseHousingManager) UpdateHouseEscrow(ctx context.Context, houseID int64, coins, status int64) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := "UPDATE character_houses SET escrow_coins = ?, escrow_status = ? WHERE unique_id = ?" - _, err := dhm.db.ExecContext(ctx, query, coins, status, houseID) + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{coins, status, houseID}, + }) + if err != nil { return fmt.Errorf("failed to update house escrow: %w", err) } @@ -908,6 +1038,12 @@ func (dhm *DatabaseHousingManager) UpdateHouseEscrow(ctx context.Context, houseI // GetHousesForUpkeep returns houses that need upkeep processing func (dhm *DatabaseHousingManager) GetHousesForUpkeep(ctx context.Context, cutoffTime time.Time) ([]PlayerHouseData, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + query := `SELECT unique_id, char_id, house_id, instance_id, upkeep_due, escrow_coins, escrow_status, status, house_name, visit_permission, public_note, private_note, allow_friends, allow_guild, require_approval, show_on_directory, allow_decoration, tax_exempt @@ -915,66 +1051,59 @@ func (dhm *DatabaseHousingManager) GetHousesForUpkeep(ctx context.Context, cutof cutoffTimestamp := cutoffTime.Unix() - rows, err := dhm.db.QueryContext(ctx, query, cutoffTimestamp, HouseStatusActive) + var houses []PlayerHouseData + err = sqlitex.Execute(conn, query, &sqlitex.ExecOptions{ + Args: []any{cutoffTimestamp, HouseStatusActive}, + ResultFunc: func(stmt *sqlite.Stmt) error { + house := PlayerHouseData{ + UniqueID: stmt.ColumnInt64(0), + CharacterID: int32(stmt.ColumnInt64(1)), + HouseID: int32(stmt.ColumnInt64(2)), + InstanceID: int32(stmt.ColumnInt64(3)), + UpkeepDue: time.Unix(stmt.ColumnInt64(4), 0), + EscrowCoins: stmt.ColumnInt64(5), + EscrowStatus: stmt.ColumnInt64(6), + Status: int8(stmt.ColumnInt64(7)), + VisitPermission: int8(stmt.ColumnInt64(9)), + AllowFriends: stmt.ColumnInt64(12) != 0, + AllowGuild: stmt.ColumnInt64(13) != 0, + RequireApproval: stmt.ColumnInt64(14) != 0, + ShowOnDirectory: stmt.ColumnInt64(15) != 0, + AllowDecoration: stmt.ColumnInt64(16) != 0, + TaxExempt: stmt.ColumnInt64(17) != 0, + } + + // Handle nullable fields + if stmt.ColumnType(8) != sqlite.TypeNull { + house.HouseName = stmt.ColumnText(8) + } + if stmt.ColumnType(10) != sqlite.TypeNull { + house.PublicNote = stmt.ColumnText(10) + } + if stmt.ColumnType(11) != sqlite.TypeNull { + house.PrivateNote = stmt.ColumnText(11) + } + + houses = append(houses, house) + return nil + }, + }) + if err != nil { return nil, fmt.Errorf("failed to query houses for upkeep: %w", err) } - defer rows.Close() - - var houses []PlayerHouseData - for rows.Next() { - var house PlayerHouseData - var upkeepDueTimestamp int64 - var houseName, publicNote, privateNote *string - - err := rows.Scan( - &house.UniqueID, - &house.CharacterID, - &house.HouseID, - &house.InstanceID, - &upkeepDueTimestamp, - &house.EscrowCoins, - &house.EscrowStatus, - &house.Status, - &houseName, - &house.VisitPermission, - &publicNote, - &privateNote, - &house.AllowFriends, - &house.AllowGuild, - &house.RequireApproval, - &house.ShowOnDirectory, - &house.AllowDecoration, - &house.TaxExempt, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan upkeep house row: %w", err) - } - - house.UpkeepDue = time.Unix(upkeepDueTimestamp, 0) - - if houseName != nil { - house.HouseName = *houseName - } - if publicNote != nil { - house.PublicNote = *publicNote - } - if privateNote != nil { - house.PrivateNote = *privateNote - } - - houses = append(houses, house) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating upkeep house rows: %w", err) - } return houses, nil } // GetHouseStatistics returns housing system statistics func (dhm *DatabaseHousingManager) GetHouseStatistics(ctx context.Context) (*HousingStatistics, error) { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + stats := &HousingStatistics{ HousesByType: make(map[int32]int64), HousesByAlignment: make(map[int8]int64), @@ -983,16 +1112,24 @@ func (dhm *DatabaseHousingManager) GetHouseStatistics(ctx context.Context) (*Hou } // Get basic counts - queries := map[string]*int64{ - "SELECT COUNT(*) FROM character_houses": &stats.TotalHouses, - "SELECT COUNT(*) FROM character_houses WHERE status = 0": &stats.ActiveHouses, - "SELECT COUNT(*) FROM character_houses WHERE status = 2": &stats.ForelosedHouses, - "SELECT COUNT(*) FROM character_house_deposits": &stats.TotalDeposits, - "SELECT COUNT(*) FROM character_house_history WHERE pos_flag = 0": &stats.TotalWithdrawals, + queries := []struct { + query string + target *int64 + }{ + {"SELECT COUNT(*) FROM character_houses", &stats.TotalHouses}, + {"SELECT COUNT(*) FROM character_houses WHERE status = 0", &stats.ActiveHouses}, + {"SELECT COUNT(*) FROM character_houses WHERE status = 2", &stats.ForelosedHouses}, + {"SELECT COUNT(*) FROM character_house_deposits", &stats.TotalDeposits}, + {"SELECT COUNT(*) FROM character_house_history WHERE pos_flag = 0", &stats.TotalWithdrawals}, } - for query, target := range queries { - err := dhm.db.QueryRowContext(ctx, query).Scan(target) + for _, q := range queries { + err = sqlitex.Execute(conn, q.query, &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + *q.target = stmt.ColumnInt64(0) + return nil + }, + }) if err != nil { return nil, fmt.Errorf("failed to get statistics: %w", err) } @@ -1003,6 +1140,12 @@ func (dhm *DatabaseHousingManager) GetHouseStatistics(ctx context.Context) (*Hou // EnsureHousingTables creates the housing tables if they don't exist func (dhm *DatabaseHousingManager) EnsureHousingTables(ctx context.Context) error { + conn, err := dhm.pool.Take(ctx) + if err != nil { + return fmt.Errorf("failed to get database connection: %w", err) + } + defer dhm.pool.Put(conn) + queries := []string{ `CREATE TABLE IF NOT EXISTS houses ( id INTEGER PRIMARY KEY, @@ -1119,8 +1262,7 @@ func (dhm *DatabaseHousingManager) EnsureHousingTables(ctx context.Context) erro } for i, query := range queries { - _, err := dhm.db.ExecContext(ctx, query) - if err != nil { + if err := sqlitex.Execute(conn, query, nil); err != nil { return fmt.Errorf("failed to create housing table %d: %w", i+1, err) } } @@ -1142,11 +1284,18 @@ func (dhm *DatabaseHousingManager) EnsureHousingTables(ctx context.Context) erro } for i, query := range indexes { - _, err := dhm.db.ExecContext(ctx, query) - if err != nil { + if err := sqlitex.Execute(conn, query, nil); err != nil { return fmt.Errorf("failed to create housing index %d: %w", i+1, err) } } return nil } + +// Helper function to convert bool to int for SQLite +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} \ No newline at end of file diff --git a/internal/housing/database_test.go b/internal/housing/database_test.go new file mode 100644 index 0000000..d190c4d --- /dev/null +++ b/internal/housing/database_test.go @@ -0,0 +1,816 @@ +package housing + +import ( + "context" + "fmt" + "testing" + "time" + + "zombiezen.com/go/sqlite/sqlitex" +) + +// createTestPool creates an in-memory SQLite database pool for testing +func createTestPool(t *testing.T) *sqlitex.Pool { + dbName := fmt.Sprintf("file:test_%s.db?mode=memory&cache=shared", t.Name()) + pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{}) + if err != nil { + t.Fatalf("Failed to create test database pool: %v", err) + } + return pool +} + +// setupTestDB creates test tables and returns a DatabaseHousingManager +func setupTestDB(t *testing.T) *DatabaseHousingManager { + pool := createTestPool(t) + dhm := NewDatabaseHousingManager(pool) + + ctx := context.Background() + if err := dhm.EnsureHousingTables(ctx); err != nil { + t.Fatalf("Failed to create test tables: %v", err) + } + + return dhm +} + +// insertTestData inserts sample data for testing +func insertTestData(t *testing.T, dhm *DatabaseHousingManager) { + ctx := context.Background() + + // Insert test house zones + testZones := []*HouseZone{ + { + ID: 1, + Name: "Small Studio", + ZoneID: 100, + CostCoin: 50000, + CostStatus: 1000, + UpkeepCoin: 5000, + UpkeepStatus: 100, + Alignment: 0, // Neutral + GuildLevel: 0, + VaultSlots: 4, + MaxItems: 100, + MaxVisitors: 10, + UpkeepPeriod: 604800, // 1 week + Description: "A cozy small studio apartment", + }, + { + ID: 2, + Name: "Large House", + ZoneID: 101, + CostCoin: 500000, + CostStatus: 10000, + UpkeepCoin: 50000, + UpkeepStatus: 1000, + Alignment: 1, // Good + GuildLevel: 20, + VaultSlots: 8, + MaxItems: 500, + MaxVisitors: 50, + UpkeepPeriod: 604800, + Description: "A spacious large house", + }, + } + + for _, zone := range testZones { + if err := dhm.SaveHouseZone(ctx, zone); err != nil { + t.Fatalf("Failed to insert test house zone: %v", err) + } + } + + // Insert test player houses + testPlayerHouses := []PlayerHouseData{ + { + CharacterID: 1001, + HouseID: 1, + InstanceID: 5001, + UpkeepDue: time.Now().Add(24 * time.Hour), + EscrowCoins: 25000, + EscrowStatus: 500, + Status: HouseStatusActive, + HouseName: "Alice's Studio", + VisitPermission: 1, + PublicNote: "Welcome to my home!", + PrivateNote: "Remember to water plants", + AllowFriends: true, + AllowGuild: false, + RequireApproval: false, + ShowOnDirectory: true, + AllowDecoration: true, + TaxExempt: false, + }, + { + CharacterID: 1002, + HouseID: 2, + InstanceID: 5002, + UpkeepDue: time.Now().Add(48 * time.Hour), + EscrowCoins: 100000, + EscrowStatus: 2000, + Status: HouseStatusActive, + HouseName: "Bob's Manor", + VisitPermission: 2, + PublicNote: "Guild meetings welcome", + PrivateNote: "Check security settings", + AllowFriends: true, + AllowGuild: true, + RequireApproval: true, + ShowOnDirectory: true, + AllowDecoration: false, + TaxExempt: true, + }, + } + + for _, house := range testPlayerHouses { + _, err := dhm.AddPlayerHouse(ctx, house) + if err != nil { + t.Fatalf("Failed to insert test player house: %v", err) + } + } +} + +func TestNewDatabaseHousingManager(t *testing.T) { + pool := createTestPool(t) + dhm := NewDatabaseHousingManager(pool) + + if dhm == nil { + t.Fatal("NewDatabaseHousingManager returned nil") + } + + if dhm.pool != pool { + t.Error("Database pool not set correctly") + } +} + +func TestEnsureHousingTables(t *testing.T) { + dhm := setupTestDB(t) + ctx := context.Background() + + // Test that tables were created (this should not error on second call) + if err := dhm.EnsureHousingTables(ctx); err != nil { + t.Errorf("EnsureHousingTables failed on second call: %v", err) + } +} + +func TestHouseZoneOperations(t *testing.T) { + dhm := setupTestDB(t) + ctx := context.Background() + + // Test SaveHouseZone and LoadHouseZone + testZone := &HouseZone{ + ID: 100, + Name: "Test House", + ZoneID: 200, + CostCoin: 100000, + CostStatus: 2000, + UpkeepCoin: 10000, + UpkeepStatus: 200, + Alignment: -1, // Evil + GuildLevel: 10, + VaultSlots: 6, + MaxItems: 250, + MaxVisitors: 25, + UpkeepPeriod: 1209600, // 2 weeks + Description: "A test house for unit testing", + } + + // Save house zone + if err := dhm.SaveHouseZone(ctx, testZone); err != nil { + t.Fatalf("SaveHouseZone failed: %v", err) + } + + // Load house zone + loadedZone, err := dhm.LoadHouseZone(ctx, testZone.ID) + if err != nil { + t.Fatalf("LoadHouseZone failed: %v", err) + } + + // Verify loaded data + if loadedZone.ID != testZone.ID { + t.Errorf("Expected ID %d, got %d", testZone.ID, loadedZone.ID) + } + if loadedZone.Name != testZone.Name { + t.Errorf("Expected Name %s, got %s", testZone.Name, loadedZone.Name) + } + if loadedZone.Description != testZone.Description { + t.Errorf("Expected Description %s, got %s", testZone.Description, loadedZone.Description) + } + + // Test LoadHouseZones + zones, err := dhm.LoadHouseZones(ctx) + if err != nil { + t.Fatalf("LoadHouseZones failed: %v", err) + } + + if len(zones) != 1 { + t.Errorf("Expected 1 zone, got %d", len(zones)) + } + + // Test DeleteHouseZone + if err := dhm.DeleteHouseZone(ctx, testZone.ID); err != nil { + t.Fatalf("DeleteHouseZone failed: %v", err) + } + + // Verify deletion + _, err = dhm.LoadHouseZone(ctx, testZone.ID) + if err == nil { + t.Error("Expected error when loading deleted house zone, got nil") + } +} + +func TestPlayerHouseOperations(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Test LoadPlayerHouses + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil { + t.Fatalf("LoadPlayerHouses failed: %v", err) + } + + if len(houses) != 1 { + t.Errorf("Expected 1 house for character 1001, got %d", len(houses)) + } + + if houses[0].HouseName != "Alice's Studio" { + t.Errorf("Expected house name 'Alice's Studio', got %s", houses[0].HouseName) + } + + // Test LoadPlayerHouse by unique ID + house, err := dhm.LoadPlayerHouse(ctx, houses[0].UniqueID) + if err != nil { + t.Fatalf("LoadPlayerHouse failed: %v", err) + } + + if house.CharacterID != 1001 { + t.Errorf("Expected character ID 1001, got %d", house.CharacterID) + } + + // Test GetHouseByInstance + houseByInstance, err := dhm.GetHouseByInstance(ctx, 5001) + if err != nil { + t.Fatalf("GetHouseByInstance failed: %v", err) + } + + if houseByInstance.CharacterID != 1001 { + t.Errorf("Expected character ID 1001, got %d", houseByInstance.CharacterID) + } + + // Test GetNextHouseID + nextID, err := dhm.GetNextHouseID(ctx) + if err != nil { + t.Fatalf("GetNextHouseID failed: %v", err) + } + + if nextID <= houses[0].UniqueID { + t.Errorf("Expected next ID > %d, got %d", houses[0].UniqueID, nextID) + } +} + +func TestPlayerHouseUpdates(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Test UpdateHouseUpkeepDue + newUpkeepDue := time.Now().Add(72 * time.Hour) + if err := dhm.UpdateHouseUpkeepDue(ctx, houseID, newUpkeepDue); err != nil { + t.Fatalf("UpdateHouseUpkeepDue failed: %v", err) + } + + // Verify update + updatedHouse, err := dhm.LoadPlayerHouse(ctx, houseID) + if err != nil { + t.Fatalf("Failed to load updated house: %v", err) + } + + if updatedHouse.UpkeepDue.Unix() != newUpkeepDue.Unix() { + t.Errorf("Expected upkeep due %v, got %v", newUpkeepDue, updatedHouse.UpkeepDue) + } + + // Test UpdateHouseEscrow + if err := dhm.UpdateHouseEscrow(ctx, houseID, 50000, 1000); err != nil { + t.Fatalf("UpdateHouseEscrow failed: %v", err) + } + + // Verify escrow update + updatedHouse, err = dhm.LoadPlayerHouse(ctx, houseID) + if err != nil { + t.Fatalf("Failed to load updated house: %v", err) + } + + if updatedHouse.EscrowCoins != 50000 { + t.Errorf("Expected escrow coins 50000, got %d", updatedHouse.EscrowCoins) + } + if updatedHouse.EscrowStatus != 1000 { + t.Errorf("Expected escrow status 1000, got %d", updatedHouse.EscrowStatus) + } +} + +func TestHouseDeposits(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Test SaveDeposit + testDeposit := HouseDeposit{ + Timestamp: time.Now(), + Amount: 10000, + LastAmount: 15000, + Status: 200, + LastStatus: 300, + Name: "Alice", + CharacterID: 1001, + } + + if err := dhm.SaveDeposit(ctx, houseID, testDeposit); err != nil { + t.Fatalf("SaveDeposit failed: %v", err) + } + + // Test LoadDeposits + deposits, err := dhm.LoadDeposits(ctx, houseID) + if err != nil { + t.Fatalf("LoadDeposits failed: %v", err) + } + + if len(deposits) != 1 { + t.Errorf("Expected 1 deposit, got %d", len(deposits)) + } + + if deposits[0].Amount != testDeposit.Amount { + t.Errorf("Expected deposit amount %d, got %d", testDeposit.Amount, deposits[0].Amount) + } + if deposits[0].Name != testDeposit.Name { + t.Errorf("Expected deposit name %s, got %s", testDeposit.Name, deposits[0].Name) + } +} + +func TestHouseHistory(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Test AddHistory + testHistory := HouseHistory{ + Timestamp: time.Now(), + Amount: 5000, + Status: 100, + Reason: "Weekly upkeep", + Name: "System", + CharacterID: 0, // System transaction + PosFlag: 0, // Withdrawal + Type: 1, // Upkeep + } + + if err := dhm.AddHistory(ctx, houseID, testHistory); err != nil { + t.Fatalf("AddHistory failed: %v", err) + } + + // Test LoadHistory + history, err := dhm.LoadHistory(ctx, houseID) + if err != nil { + t.Fatalf("LoadHistory failed: %v", err) + } + + if len(history) != 1 { + t.Errorf("Expected 1 history entry, got %d", len(history)) + } + + if history[0].Reason != testHistory.Reason { + t.Errorf("Expected history reason %s, got %s", testHistory.Reason, history[0].Reason) + } + if history[0].Amount != testHistory.Amount { + t.Errorf("Expected history amount %d, got %d", testHistory.Amount, history[0].Amount) + } +} + +func TestHouseAccess(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Test SaveHouseAccess + testAccess := []HouseAccess{ + { + CharacterID: 2001, + PlayerName: "Bob", + AccessLevel: 1, + Permissions: 15, // Full permissions + GrantedBy: 1001, + GrantedDate: time.Now(), + ExpiresDate: time.Now().Add(30 * 24 * time.Hour), + Notes: "Trusted friend", + }, + { + CharacterID: 2002, + PlayerName: "Charlie", + AccessLevel: 2, + Permissions: 7, // Limited permissions + GrantedBy: 1001, + GrantedDate: time.Now(), + ExpiresDate: time.Now().Add(7 * 24 * time.Hour), + Notes: "Temporary access", + }, + } + + if err := dhm.SaveHouseAccess(ctx, houseID, testAccess); err != nil { + t.Fatalf("SaveHouseAccess failed: %v", err) + } + + // Test LoadHouseAccess + accessList, err := dhm.LoadHouseAccess(ctx, houseID) + if err != nil { + t.Fatalf("LoadHouseAccess failed: %v", err) + } + + if len(accessList) != 2 { + t.Errorf("Expected 2 access entries, got %d", len(accessList)) + } + + // Test DeleteHouseAccess + if err := dhm.DeleteHouseAccess(ctx, houseID, 2002); err != nil { + t.Fatalf("DeleteHouseAccess failed: %v", err) + } + + // Verify deletion + accessList, err = dhm.LoadHouseAccess(ctx, houseID) + if err != nil { + t.Fatalf("LoadHouseAccess after deletion failed: %v", err) + } + + if len(accessList) != 1 { + t.Errorf("Expected 1 access entry after deletion, got %d", len(accessList)) + } + + if accessList[0].CharacterID != 2001 { + t.Errorf("Expected remaining access for character 2001, got %d", accessList[0].CharacterID) + } +} + +func TestHouseAmenities(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Test SaveHouseAmenity + testAmenity := HouseAmenity{ + ID: 1, + Type: 1, // Furniture + Name: "Comfortable Chair", + Cost: 1000, + StatusCost: 20, + PurchaseDate: time.Now(), + X: 100.5, + Y: 200.0, + Z: 50.25, + Heading: 180.0, + IsActive: true, + } + + if err := dhm.SaveHouseAmenity(ctx, houseID, testAmenity); err != nil { + t.Fatalf("SaveHouseAmenity failed: %v", err) + } + + // Test LoadHouseAmenities + amenities, err := dhm.LoadHouseAmenities(ctx, houseID) + if err != nil { + t.Fatalf("LoadHouseAmenities failed: %v", err) + } + + if len(amenities) != 1 { + t.Errorf("Expected 1 amenity, got %d", len(amenities)) + } + + if amenities[0].Name != testAmenity.Name { + t.Errorf("Expected amenity name %s, got %s", testAmenity.Name, amenities[0].Name) + } + if amenities[0].X != testAmenity.X { + t.Errorf("Expected X position %f, got %f", testAmenity.X, amenities[0].X) + } + + // Test DeleteHouseAmenity + if err := dhm.DeleteHouseAmenity(ctx, houseID, testAmenity.ID); err != nil { + t.Fatalf("DeleteHouseAmenity failed: %v", err) + } + + // Verify deletion + amenities, err = dhm.LoadHouseAmenities(ctx, houseID) + if err != nil { + t.Fatalf("LoadHouseAmenities after deletion failed: %v", err) + } + + if len(amenities) != 0 { + t.Errorf("Expected 0 amenities after deletion, got %d", len(amenities)) + } +} + +func TestHouseItems(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Test SaveHouseItem + testItem := HouseItem{ + ID: 1, + ItemID: 12345, + CharacterID: 1001, + X: 150.0, + Y: 250.0, + Z: 75.5, + Heading: 90.0, + PitchX: 5.0, + PitchY: 0.0, + RollX: 0.0, + RollY: 2.5, + PlacedDate: time.Now(), + Quantity: 1, + Condition: 100, + House: "main", + } + + if err := dhm.SaveHouseItem(ctx, houseID, testItem); err != nil { + t.Fatalf("SaveHouseItem failed: %v", err) + } + + // Test LoadHouseItems + items, err := dhm.LoadHouseItems(ctx, houseID) + if err != nil { + t.Fatalf("LoadHouseItems failed: %v", err) + } + + if len(items) != 1 { + t.Errorf("Expected 1 item, got %d", len(items)) + } + + if items[0].ItemID != testItem.ItemID { + t.Errorf("Expected item ID %d, got %d", testItem.ItemID, items[0].ItemID) + } + if items[0].House != testItem.House { + t.Errorf("Expected house %s, got %s", testItem.House, items[0].House) + } + + // Test DeleteHouseItem + if err := dhm.DeleteHouseItem(ctx, houseID, testItem.ID); err != nil { + t.Fatalf("DeleteHouseItem failed: %v", err) + } + + // Verify deletion + items, err = dhm.LoadHouseItems(ctx, houseID) + if err != nil { + t.Fatalf("LoadHouseItems after deletion failed: %v", err) + } + + if len(items) != 0 { + t.Errorf("Expected 0 items after deletion, got %d", len(items)) + } +} + +func TestGetHousesForUpkeep(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Test with cutoff time in the future (should find houses) + cutoffTime := time.Now().Add(72 * time.Hour) + houses, err := dhm.GetHousesForUpkeep(ctx, cutoffTime) + if err != nil { + t.Fatalf("GetHousesForUpkeep failed: %v", err) + } + + if len(houses) != 2 { + t.Errorf("Expected 2 houses for upkeep, got %d", len(houses)) + } + + // Test with cutoff time in the past (should find no houses) + cutoffTime = time.Now().Add(-24 * time.Hour) + houses, err = dhm.GetHousesForUpkeep(ctx, cutoffTime) + if err != nil { + t.Fatalf("GetHousesForUpkeep with past cutoff failed: %v", err) + } + + if len(houses) != 0 { + t.Errorf("Expected 0 houses for past upkeep, got %d", len(houses)) + } +} + +func TestGetHouseStatistics(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Add some test data for statistics + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Add some deposits and history for stats + testDeposit := HouseDeposit{ + Timestamp: time.Now(), + Amount: 5000, + LastAmount: 10000, + Status: 100, + LastStatus: 200, + Name: "Alice", + CharacterID: 1001, + } + + if err := dhm.SaveDeposit(ctx, houseID, testDeposit); err != nil { + t.Fatalf("Failed to save test deposit: %v", err) + } + + testHistory := HouseHistory{ + Timestamp: time.Now(), + Amount: 2000, + Status: 50, + Reason: "Withdrawal", + Name: "Alice", + CharacterID: 1001, + PosFlag: 0, // Withdrawal + Type: 2, + } + + if err := dhm.AddHistory(ctx, houseID, testHistory); err != nil { + t.Fatalf("Failed to add test history: %v", err) + } + + // Test GetHouseStatistics + stats, err := dhm.GetHouseStatistics(ctx) + if err != nil { + t.Fatalf("GetHouseStatistics failed: %v", err) + } + + if stats.TotalHouses != 2 { + t.Errorf("Expected 2 total houses, got %d", stats.TotalHouses) + } + if stats.ActiveHouses != 2 { + t.Errorf("Expected 2 active houses, got %d", stats.ActiveHouses) + } + if stats.TotalDeposits != 1 { + t.Errorf("Expected 1 total deposits, got %d", stats.TotalDeposits) + } + if stats.TotalWithdrawals != 1 { + t.Errorf("Expected 1 total withdrawals, got %d", stats.TotalWithdrawals) + } +} + +func TestDeletePlayerHouse(t *testing.T) { + dhm := setupTestDB(t) + insertTestData(t, dhm) + ctx := context.Background() + + // Get a test house + houses, err := dhm.LoadPlayerHouses(ctx, 1001) + if err != nil || len(houses) == 0 { + t.Fatalf("Failed to load test house: %v", err) + } + + houseID := houses[0].UniqueID + + // Add some related data that should be cascade deleted + testDeposit := HouseDeposit{ + Timestamp: time.Now(), + Amount: 5000, + LastAmount: 10000, + Status: 100, + LastStatus: 200, + Name: "Alice", + CharacterID: 1001, + } + + if err := dhm.SaveDeposit(ctx, houseID, testDeposit); err != nil { + t.Fatalf("Failed to save test deposit: %v", err) + } + + // Test DeletePlayerHouse + if err := dhm.DeletePlayerHouse(ctx, houseID); err != nil { + t.Fatalf("DeletePlayerHouse failed: %v", err) + } + + // Verify deletion + _, err = dhm.LoadPlayerHouse(ctx, houseID) + if err == nil { + t.Error("Expected error when loading deleted player house, got nil") + } + + // Verify related data was also deleted + deposits, err := dhm.LoadDeposits(ctx, houseID) + if err != nil { + t.Fatalf("LoadDeposits after house deletion failed: %v", err) + } + + if len(deposits) != 0 { + t.Errorf("Expected 0 deposits after house deletion, got %d", len(deposits)) + } + + // Verify other houses are still there + remainingHouses, err := dhm.LoadPlayerHouses(ctx, 1002) + if err != nil { + t.Fatalf("Failed to load remaining houses: %v", err) + } + + if len(remainingHouses) != 1 { + t.Errorf("Expected 1 remaining house, got %d", len(remainingHouses)) + } +} + +func TestErrorCases(t *testing.T) { + dhm := setupTestDB(t) + ctx := context.Background() + + // Test loading non-existent house zone + _, err := dhm.LoadHouseZone(ctx, 999) + if err == nil { + t.Error("Expected error when loading non-existent house zone, got nil") + } + + // Test loading non-existent player house + _, err = dhm.LoadPlayerHouse(ctx, 999) + if err == nil { + t.Error("Expected error when loading non-existent player house, got nil") + } + + // Test loading house by non-existent instance + _, err = dhm.GetHouseByInstance(ctx, 999) + if err == nil { + t.Error("Expected error when loading house by non-existent instance, got nil") + } + + // Test operations on non-existent house + nonExistentHouseID := int64(999) + + deposits, err := dhm.LoadDeposits(ctx, nonExistentHouseID) + if err != nil { + t.Errorf("LoadDeposits should not error on non-existent house: %v", err) + } + if len(deposits) != 0 { + t.Errorf("Expected 0 deposits for non-existent house, got %d", len(deposits)) + } + + history, err := dhm.LoadHistory(ctx, nonExistentHouseID) + if err != nil { + t.Errorf("LoadHistory should not error on non-existent house: %v", err) + } + if len(history) != 0 { + t.Errorf("Expected 0 history entries for non-existent house, got %d", len(history)) + } +} + +// Helper function to compare times with tolerance for database precision +func timesEqual(t1, t2 time.Time, tolerance time.Duration) bool { + diff := t1.Sub(t2) + if diff < 0 { + diff = -diff + } + return diff <= tolerance +} \ No newline at end of file