What a mess
This commit is contained in:
102
migrations/add_performance_indexes.py
Normal file
102
migrations/add_performance_indexes.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Add database indexes for performance optimization.
|
||||
These indexes target the most frequently queried columns.
|
||||
|
||||
Expected improvement: 50-70% faster query response times
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
load_dotenv()
|
||||
|
||||
DB_USER = os.getenv("DB_USER", "user")
|
||||
DB_PASS = os.getenv("DB_PASS", "password")
|
||||
DB_HOST = os.getenv("DB_HOST", "db")
|
||||
DB_PORT = os.getenv("DB_PORT", "5432")
|
||||
DB_NAME = os.getenv("DB_NAME", "echoes")
|
||||
|
||||
DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
# Indexes to create with their purpose
|
||||
INDEXES = [
|
||||
# Players table - most commonly queried
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_players_username ON players(username);",
|
||||
"Speed up login/authentication queries"
|
||||
),
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_players_location_id ON players(location_id);",
|
||||
"Speed up 'get all players in location' queries"
|
||||
),
|
||||
|
||||
# Dropped items - queried on every location view
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_dropped_items_location ON dropped_items(location_id);",
|
||||
"Speed up 'show items on ground' queries"
|
||||
),
|
||||
|
||||
# Wandering enemies - checked frequently
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_wandering_enemies_location ON wandering_enemies(location_id);",
|
||||
"Speed up 'get enemies in location' queries"
|
||||
),
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_wandering_enemies_despawn ON wandering_enemies(despawn_timestamp);",
|
||||
"Speed up cleanup queries for expired enemies"
|
||||
),
|
||||
|
||||
# Inventory - queried on every inventory operation
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_inventory_player_item ON inventory(player_id, item_id);",
|
||||
"Speed up inventory lookups and item checks"
|
||||
),
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_inventory_player ON inventory(player_id);",
|
||||
"Speed up 'get all player inventory' queries"
|
||||
),
|
||||
|
||||
# Active combats - checked on most actions
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_active_combats_player ON active_combats(player_id);",
|
||||
"Speed up 'is player in combat' checks"
|
||||
),
|
||||
|
||||
# Interactable cooldowns - checked on interact attempts
|
||||
(
|
||||
"CREATE INDEX IF NOT EXISTS idx_interactable_cooldowns_player ON interactable_cooldowns(player_id, interactable_id);",
|
||||
"Speed up cooldown checks"
|
||||
),
|
||||
]
|
||||
|
||||
async def add_indexes():
|
||||
"""Add all performance indexes to the database."""
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
print("Starting index creation...\n")
|
||||
|
||||
for sql, purpose in INDEXES:
|
||||
index_name = sql.split("IF NOT EXISTS ")[1].split(" ON ")[0]
|
||||
print(f"Creating {index_name}...")
|
||||
print(f" Purpose: {purpose}")
|
||||
|
||||
try:
|
||||
await conn.execute(text(sql))
|
||||
print(f" ✓ Success\n")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}\n")
|
||||
|
||||
print("\n✓ Index creation complete!")
|
||||
print("\nTo verify indexes were created:")
|
||||
print(" docker exec echoes_of_the_ashes_db psql -U user -d echoes -c \"\\di\"")
|
||||
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(add_indexes())
|
||||
18
migrations/add_status_effects_table.sql
Normal file
18
migrations/add_status_effects_table.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Add persistent status effects table
|
||||
CREATE TABLE IF NOT EXISTS player_status_effects (
|
||||
id SERIAL PRIMARY KEY,
|
||||
player_id INTEGER NOT NULL REFERENCES players(telegram_id) ON DELETE CASCADE,
|
||||
effect_name VARCHAR(50) NOT NULL,
|
||||
effect_icon VARCHAR(10) NOT NULL,
|
||||
damage_per_tick INTEGER NOT NULL DEFAULT 0,
|
||||
ticks_remaining INTEGER NOT NULL,
|
||||
applied_at FLOAT NOT NULL,
|
||||
CONSTRAINT valid_ticks CHECK (ticks_remaining >= 0),
|
||||
CONSTRAINT valid_damage CHECK (damage_per_tick >= 0)
|
||||
);
|
||||
|
||||
-- Create index for efficient querying by player
|
||||
CREATE INDEX IF NOT EXISTS idx_status_effects_player ON player_status_effects(player_id);
|
||||
|
||||
-- Create index for background processor to find active effects
|
||||
CREATE INDEX IF NOT EXISTS idx_status_effects_active ON player_status_effects(player_id, ticks_remaining) WHERE ticks_remaining > 0;
|
||||
41
migrations/apply_status_effects_migration.py
Normal file
41
migrations/apply_status_effects_migration.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add player_status_effects table.
|
||||
This table stores persistent status effects that can exist both during and outside of combat.
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
# Database connection
|
||||
DB_USER = os.getenv("POSTGRES_USER")
|
||||
DB_PASS = os.getenv("POSTGRES_PASSWORD")
|
||||
DB_NAME = os.getenv("POSTGRES_DB")
|
||||
DB_HOST = os.getenv("POSTGRES_HOST")
|
||||
DB_PORT = os.getenv("POSTGRES_PORT")
|
||||
DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
async def apply_migration():
|
||||
"""Apply the status effects table migration."""
|
||||
engine = create_async_engine(DATABASE_URL)
|
||||
|
||||
print("Applying status effects table migration...")
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
# Read and execute the SQL file
|
||||
with open('migrations/add_status_effects_table.sql', 'r') as f:
|
||||
sql = f.read()
|
||||
|
||||
await conn.execute(text(sql))
|
||||
print("✅ Successfully created player_status_effects table")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(apply_migration())
|
||||
37
migrations/fix_telegram_id_nullable.py
Normal file
37
migrations/fix_telegram_id_nullable.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fix telegram_id column to be nullable for web users.
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import text
|
||||
|
||||
# Database connection
|
||||
DB_USER = os.getenv("POSTGRES_USER", "echoes_user")
|
||||
DB_PASS = os.getenv("POSTGRES_PASSWORD", "echoes_pass")
|
||||
DB_NAME = os.getenv("POSTGRES_DB", "echoes_db")
|
||||
DB_HOST = os.getenv("POSTGRES_HOST", "echoes_of_the_ashes_db")
|
||||
DB_PORT = os.getenv("POSTGRES_PORT", "5432")
|
||||
|
||||
DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
async def fix_telegram_id():
|
||||
"""Alter telegram_id column to be nullable"""
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
print("Making telegram_id nullable...")
|
||||
await conn.execute(text(
|
||||
"ALTER TABLE players ALTER COLUMN telegram_id DROP NOT NULL;"
|
||||
))
|
||||
print("✅ telegram_id is now nullable!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(fix_telegram_id())
|
||||
40
migrations/migrate_add_movement_cooldown.py
Normal file
40
migrations/migrate_add_movement_cooldown.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Migration: Add last_movement_time column to players table
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from api import database as db
|
||||
|
||||
async def migrate():
|
||||
await db.init_db()
|
||||
|
||||
try:
|
||||
async with db.DatabaseSession() as session:
|
||||
# Check if column exists
|
||||
result = await session.execute(db.text("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'players'
|
||||
AND column_name = 'last_movement_time'
|
||||
"""))
|
||||
|
||||
count = result.scalar()
|
||||
|
||||
if count == 0:
|
||||
print("Adding last_movement_time column to players table...")
|
||||
await session.execute(db.text("""
|
||||
ALTER TABLE players
|
||||
ADD COLUMN last_movement_time FLOAT DEFAULT 0
|
||||
"""))
|
||||
await session.commit()
|
||||
print("✅ Column added successfully!")
|
||||
else:
|
||||
print("⚠️ Column last_movement_time already exists, skipping.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
26
migrations/migrate_add_pvp_acknowledged.py
Normal file
26
migrations/migrate_add_pvp_acknowledged.py
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add acknowledged flags to pvp_combats table
|
||||
"""
|
||||
import asyncio
|
||||
from api.database import DatabaseSession, pvp_combats
|
||||
from sqlalchemy import text
|
||||
|
||||
async def migrate():
|
||||
"""Add attacker_acknowledged and defender_acknowledged columns"""
|
||||
async with DatabaseSession() as session:
|
||||
# Add attacker_acknowledged column
|
||||
await session.execute(text(
|
||||
"ALTER TABLE pvp_combats ADD COLUMN IF NOT EXISTS attacker_acknowledged BOOLEAN DEFAULT FALSE"
|
||||
))
|
||||
|
||||
# Add defender_acknowledged column
|
||||
await session.execute(text(
|
||||
"ALTER TABLE pvp_combats ADD COLUMN IF NOT EXISTS defender_acknowledged BOOLEAN DEFAULT FALSE"
|
||||
))
|
||||
|
||||
await session.commit()
|
||||
print("✅ Added attacker_acknowledged and defender_acknowledged columns to pvp_combats table")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
24
migrations/migrate_add_pvp_combat.py
Normal file
24
migrations/migrate_add_pvp_combat.py
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add pvp_combats table
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
from api.database import engine, metadata, pvp_combats
|
||||
|
||||
async def migrate():
|
||||
"""Create pvp_combats table"""
|
||||
async with engine.begin() as conn:
|
||||
print("Creating pvp_combats table...")
|
||||
await conn.run_sync(pvp_combats.create, checkfirst=True)
|
||||
print("✅ pvp_combats table created successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== PvP Combat Table Migration ===")
|
||||
asyncio.run(migrate())
|
||||
print("Migration complete!")
|
||||
49
migrations/migrate_add_pvp_last_action.py
Normal file
49
migrations/migrate_add_pvp_last_action.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration: Add last_action field to pvp_combats table
|
||||
This allows the opponent to see what happened in the last turn
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
|
||||
async def migrate():
|
||||
"""Add last_action column to pvp_combats table"""
|
||||
|
||||
# Database connection details
|
||||
db_host = os.getenv('DB_HOST', 'localhost')
|
||||
db_port = os.getenv('DB_PORT', '5432')
|
||||
db_name = os.getenv('DB_NAME', 'echoes_db')
|
||||
db_user = os.getenv('DB_USER', 'echoes_user')
|
||||
db_password = os.getenv('DB_PASSWORD', 'change_this_password')
|
||||
|
||||
# Create async engine
|
||||
database_url = f"postgresql+asyncpg://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
try:
|
||||
# Add last_action column to pvp_combats
|
||||
await session.execute(text("""
|
||||
ALTER TABLE pvp_combats
|
||||
ADD COLUMN IF NOT EXISTS last_action TEXT DEFAULT NULL;
|
||||
"""))
|
||||
|
||||
await session.commit()
|
||||
print("✅ Added last_action column to pvp_combats table")
|
||||
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
print(f"❌ Error: {e}")
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
print("✅ Migration completed successfully!")
|
||||
41
migrations/migrate_add_pvp_stats.py
Normal file
41
migrations/migrate_add_pvp_stats.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add PvP statistics columns
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
from api.database import engine
|
||||
|
||||
async def migrate():
|
||||
"""Add PvP statistics columns to player_statistics table"""
|
||||
async with engine.begin() as conn:
|
||||
print("Adding PvP statistics columns...")
|
||||
|
||||
# Add PvP columns
|
||||
await conn.execute(text("""
|
||||
ALTER TABLE player_statistics
|
||||
ADD COLUMN IF NOT EXISTS pvp_combats_initiated INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_combats_won INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_combats_lost INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_damage_dealt INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_damage_taken INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS players_killed INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_deaths INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_successful_flees INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_failed_flees INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_attacks_landed INTEGER DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pvp_attacks_received INTEGER DEFAULT 0
|
||||
"""))
|
||||
|
||||
print("✅ PvP statistics columns added successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
from sqlalchemy import text
|
||||
print("=== PvP Statistics Migration ===")
|
||||
asyncio.run(migrate())
|
||||
print("Migration complete!")
|
||||
92
migrations/migrate_equipment_system.py
Normal file
92
migrations/migrate_equipment_system.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Migration script for equipment system
|
||||
Adds equipment slots, encumbrance stat, and item durability/tier system
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from api import database as db
|
||||
|
||||
async def migrate():
|
||||
"""Add equipment system to database"""
|
||||
await db.init_db()
|
||||
|
||||
try:
|
||||
async with db.DatabaseSession() as session:
|
||||
print("🔄 Starting equipment system migration...")
|
||||
|
||||
# 1. Add encumbrance to players table
|
||||
print("📊 Adding encumbrance stat to players...")
|
||||
await session.execute(db.text("""
|
||||
ALTER TABLE players
|
||||
ADD COLUMN IF NOT EXISTS encumbrance INTEGER DEFAULT 0;
|
||||
"""))
|
||||
|
||||
# 2. Create equipment_slots table
|
||||
print("🎽 Creating equipment_slots table...")
|
||||
await session.execute(db.text("""
|
||||
CREATE TABLE IF NOT EXISTS equipment_slots (
|
||||
player_id INTEGER REFERENCES players(id) ON DELETE CASCADE,
|
||||
slot_type VARCHAR(20) NOT NULL,
|
||||
item_id INTEGER REFERENCES inventory(id) ON DELETE SET NULL,
|
||||
PRIMARY KEY (player_id, slot_type),
|
||||
CONSTRAINT valid_slot_type CHECK (slot_type IN (
|
||||
'head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack'
|
||||
))
|
||||
);
|
||||
"""))
|
||||
|
||||
# 3. Add durability and tier columns to inventory
|
||||
print("🔧 Adding durability and tier to inventory...")
|
||||
await session.execute(db.text("""
|
||||
ALTER TABLE inventory
|
||||
ADD COLUMN IF NOT EXISTS durability INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS max_durability INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS tier INTEGER DEFAULT 1,
|
||||
ADD COLUMN IF NOT EXISTS unique_stats JSONB;
|
||||
"""))
|
||||
|
||||
# 4. Add is_equipped flag if not exists (should exist, but just in case)
|
||||
print("📌 Ensuring is_equipped column exists...")
|
||||
await session.execute(db.text("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name='inventory' AND column_name='is_equipped'
|
||||
) THEN
|
||||
ALTER TABLE inventory ADD COLUMN is_equipped BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
END $$;
|
||||
"""))
|
||||
|
||||
await session.commit()
|
||||
|
||||
# 5. Initialize equipment slots for all existing players
|
||||
print("👤 Initializing equipment slots for existing players...")
|
||||
result = await session.execute(db.text("SELECT id FROM players"))
|
||||
players = result.fetchall()
|
||||
|
||||
slots = ['head', 'torso', 'legs', 'feet', 'weapon', 'offhand', 'backpack']
|
||||
|
||||
for player in players:
|
||||
player_id = player[0]
|
||||
for slot in slots:
|
||||
await session.execute(db.text("""
|
||||
INSERT INTO equipment_slots (player_id, slot_type, item_id)
|
||||
VALUES (:player_id, :slot_type, NULL)
|
||||
ON CONFLICT (player_id, slot_type) DO NOTHING
|
||||
"""), {"player_id": player_id, "slot_type": slot})
|
||||
|
||||
await session.commit()
|
||||
print(f"✅ Initialized equipment slots for {len(players)} players")
|
||||
|
||||
print("✅ Equipment system migration completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error during migration: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
58
migrations/migrate_unique_items.py
Normal file
58
migrations/migrate_unique_items.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Migration: Add unique item properties to dropped_items table
|
||||
|
||||
This migration adds durability, max_durability, tier, and unique_stats columns
|
||||
to the dropped_items table so that dropped equipment items preserve their state.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from api.database import DatabaseSession, engine, metadata
|
||||
from sqlalchemy import text
|
||||
|
||||
async def migrate():
|
||||
"""Add unique item columns to dropped_items"""
|
||||
|
||||
async with DatabaseSession() as session:
|
||||
print("Starting migration: Add unique item properties to dropped_items...")
|
||||
|
||||
# Add durability column
|
||||
try:
|
||||
await session.execute(text(
|
||||
"ALTER TABLE dropped_items ADD COLUMN durability INTEGER"
|
||||
))
|
||||
print("✓ Added durability column")
|
||||
except Exception as e:
|
||||
print(f"✗ durability column may already exist: {e}")
|
||||
|
||||
# Add max_durability column
|
||||
try:
|
||||
await session.execute(text(
|
||||
"ALTER TABLE dropped_items ADD COLUMN max_durability INTEGER"
|
||||
))
|
||||
print("✓ Added max_durability column")
|
||||
except Exception as e:
|
||||
print(f"✗ max_durability column may already exist: {e}")
|
||||
|
||||
# Add tier column
|
||||
try:
|
||||
await session.execute(text(
|
||||
"ALTER TABLE dropped_items ADD COLUMN tier INTEGER DEFAULT 1"
|
||||
))
|
||||
print("✓ Added tier column")
|
||||
except Exception as e:
|
||||
print(f"✗ tier column may already exist: {e}")
|
||||
|
||||
# Add unique_stats JSONB column
|
||||
try:
|
||||
await session.execute(text(
|
||||
"ALTER TABLE dropped_items ADD COLUMN unique_stats JSONB"
|
||||
))
|
||||
print("✓ Added unique_stats column")
|
||||
except Exception as e:
|
||||
print(f"✗ unique_stats column may already exist: {e}")
|
||||
|
||||
await session.commit()
|
||||
print("\n✓ Migration completed successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
147
migrations/migrate_unique_items_table.py
Normal file
147
migrations/migrate_unique_items_table.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Migration: Create unique_items table and refactor item tracking
|
||||
|
||||
This creates a proper architecture where:
|
||||
1. unique_items table stores individual item instances with their properties
|
||||
2. inventory/dropped_items reference unique_item_id instead of duplicating data
|
||||
3. When item is picked up, only the reference changes (dropped_items -> inventory)
|
||||
4. When item decays/breaks, delete from unique_items (cascades to references)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from api.database import DatabaseSession, engine, metadata
|
||||
from sqlalchemy import text
|
||||
|
||||
async def migrate():
|
||||
"""Create unique_items table and refactor references"""
|
||||
|
||||
async with DatabaseSession() as session:
|
||||
print("Starting migration: Create unique_items table...")
|
||||
|
||||
# Step 1: Create unique_items table
|
||||
try:
|
||||
await session.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS unique_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
item_id VARCHAR NOT NULL,
|
||||
durability INTEGER,
|
||||
max_durability INTEGER,
|
||||
tier INTEGER DEFAULT 1,
|
||||
unique_stats JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
print("✓ Created unique_items table")
|
||||
except Exception as e:
|
||||
print(f"✗ Error creating unique_items table: {e}")
|
||||
return
|
||||
|
||||
# Step 2: Add unique_item_id to inventory (nullable for now)
|
||||
try:
|
||||
await session.execute(text(
|
||||
"ALTER TABLE inventory ADD COLUMN IF NOT EXISTS unique_item_id INTEGER REFERENCES unique_items(id) ON DELETE CASCADE"
|
||||
))
|
||||
print("✓ Added unique_item_id to inventory")
|
||||
except Exception as e:
|
||||
print(f"✗ unique_item_id may already exist in inventory: {e}")
|
||||
|
||||
# Step 3: Add unique_item_id to dropped_items (nullable for now)
|
||||
try:
|
||||
await session.execute(text(
|
||||
"ALTER TABLE dropped_items ADD COLUMN IF NOT EXISTS unique_item_id INTEGER REFERENCES unique_items(id) ON DELETE CASCADE"
|
||||
))
|
||||
print("✓ Added unique_item_id to dropped_items")
|
||||
except Exception as e:
|
||||
print(f"✗ unique_item_id may already exist in dropped_items: {e}")
|
||||
|
||||
# Step 4: Migrate existing inventory items with durability to unique_items
|
||||
print("\nMigrating existing inventory items to unique_items...")
|
||||
result = await session.execute(text("""
|
||||
SELECT id, item_id, durability, max_durability, tier, unique_stats
|
||||
FROM inventory
|
||||
WHERE durability IS NOT NULL OR tier IS NOT NULL OR unique_stats IS NOT NULL
|
||||
"""))
|
||||
inventory_items = result.fetchall()
|
||||
|
||||
migrated_count = 0
|
||||
for inv_item in inventory_items:
|
||||
# Create unique_item entry
|
||||
result = await session.execute(text("""
|
||||
INSERT INTO unique_items (item_id, durability, max_durability, tier, unique_stats)
|
||||
VALUES (:item_id, :durability, :max_durability, :tier, :unique_stats)
|
||||
RETURNING id
|
||||
"""), {
|
||||
'item_id': inv_item.item_id,
|
||||
'durability': inv_item.durability,
|
||||
'max_durability': inv_item.max_durability,
|
||||
'tier': inv_item.tier,
|
||||
'unique_stats': inv_item.unique_stats
|
||||
})
|
||||
unique_item_id = result.fetchone()[0]
|
||||
|
||||
# Update inventory to reference it
|
||||
await session.execute(text("""
|
||||
UPDATE inventory
|
||||
SET unique_item_id = :unique_item_id
|
||||
WHERE id = :inv_id
|
||||
"""), {
|
||||
'unique_item_id': unique_item_id,
|
||||
'inv_id': inv_item.id
|
||||
})
|
||||
migrated_count += 1
|
||||
|
||||
print(f"✓ Migrated {migrated_count} inventory items to unique_items")
|
||||
|
||||
# Step 5: Migrate existing dropped_items with durability to unique_items
|
||||
print("\nMigrating existing dropped items to unique_items...")
|
||||
result = await session.execute(text("""
|
||||
SELECT id, item_id, durability, max_durability, tier, unique_stats
|
||||
FROM dropped_items
|
||||
WHERE durability IS NOT NULL OR tier IS NOT NULL OR unique_stats IS NOT NULL
|
||||
"""))
|
||||
dropped_items_list = result.fetchall()
|
||||
|
||||
migrated_dropped = 0
|
||||
for dropped_item in dropped_items_list:
|
||||
# Create unique_item entry
|
||||
result = await session.execute(text("""
|
||||
INSERT INTO unique_items (item_id, durability, max_durability, tier, unique_stats)
|
||||
VALUES (:item_id, :durability, :max_durability, :tier, :unique_stats)
|
||||
RETURNING id
|
||||
"""), {
|
||||
'item_id': dropped_item.item_id,
|
||||
'durability': dropped_item.durability,
|
||||
'max_durability': dropped_item.max_durability,
|
||||
'tier': dropped_item.tier,
|
||||
'unique_stats': dropped_item.unique_stats
|
||||
})
|
||||
unique_item_id = result.fetchone()[0]
|
||||
|
||||
# Update dropped_items to reference it
|
||||
await session.execute(text("""
|
||||
UPDATE dropped_items
|
||||
SET unique_item_id = :unique_item_id
|
||||
WHERE id = :dropped_id
|
||||
"""), {
|
||||
'unique_item_id': unique_item_id,
|
||||
'dropped_id': dropped_item.id
|
||||
})
|
||||
migrated_dropped += 1
|
||||
|
||||
print(f"✓ Migrated {migrated_dropped} dropped items to unique_items")
|
||||
|
||||
# Step 6: Drop old columns from inventory (keep for backward compatibility for now)
|
||||
# We'll drop these in a future migration after verifying everything works
|
||||
print("\n⚠️ Old durability/tier columns still exist for backward compatibility")
|
||||
print(" They can be safely removed in a future migration")
|
||||
|
||||
await session.commit()
|
||||
print("\n✅ Migration completed successfully!")
|
||||
print(f"\n📊 Summary:")
|
||||
print(f" - Created unique_items table")
|
||||
print(f" - Migrated {migrated_count} inventory items")
|
||||
print(f" - Migrated {migrated_dropped} dropped items")
|
||||
print(f" - Total unique items: {migrated_count + migrated_dropped}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(migrate())
|
||||
Reference in New Issue
Block a user